dependabot-uv 0.299.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.
- checksums.yaml +7 -0
- data/helpers/build +34 -0
- data/helpers/lib/__init__.py +0 -0
- data/helpers/lib/hasher.py +36 -0
- data/helpers/lib/parser.py +270 -0
- data/helpers/requirements.txt +13 -0
- data/helpers/run.py +22 -0
- data/lib/dependabot/uv/authed_url_builder.rb +31 -0
- data/lib/dependabot/uv/file_fetcher.rb +328 -0
- data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
- data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
- data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
- data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
- data/lib/dependabot/uv/file_parser.rb +437 -0
- data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
- data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
- data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
- data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
- data/lib/dependabot/uv/file_updater.rb +105 -0
- data/lib/dependabot/uv/language.rb +76 -0
- data/lib/dependabot/uv/language_version_manager.rb +114 -0
- data/lib/dependabot/uv/metadata_finder.rb +186 -0
- data/lib/dependabot/uv/name_normaliser.rb +26 -0
- data/lib/dependabot/uv/native_helpers.rb +38 -0
- data/lib/dependabot/uv/package_manager.rb +54 -0
- data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
- data/lib/dependabot/uv/pipenv_runner.rb +108 -0
- data/lib/dependabot/uv/requirement.rb +163 -0
- data/lib/dependabot/uv/requirement_parser.rb +60 -0
- data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
- data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
- data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
- data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
- data/lib/dependabot/uv/update_checker.rb +317 -0
- data/lib/dependabot/uv/version.rb +321 -0
- data/lib/dependabot/uv.rb +35 -0
- metadata +306 -0
@@ -0,0 +1,506 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "open3"
|
5
|
+
require "dependabot/dependency"
|
6
|
+
require "dependabot/uv/requirement_parser"
|
7
|
+
require "dependabot/uv/file_fetcher"
|
8
|
+
require "dependabot/uv/file_parser"
|
9
|
+
require "dependabot/uv/file_parser/python_requirement_parser"
|
10
|
+
require "dependabot/uv/update_checker"
|
11
|
+
require "dependabot/uv/file_updater/requirement_replacer"
|
12
|
+
require "dependabot/uv/version"
|
13
|
+
require "dependabot/shared_helpers"
|
14
|
+
require "dependabot/uv/language_version_manager"
|
15
|
+
require "dependabot/uv/native_helpers"
|
16
|
+
require "dependabot/uv/name_normaliser"
|
17
|
+
require "dependabot/uv/authed_url_builder"
|
18
|
+
|
19
|
+
module Dependabot
|
20
|
+
module Uv
|
21
|
+
class UpdateChecker
|
22
|
+
# This class does version resolution for pip-compile. Its approach is:
|
23
|
+
# - Unlock the dependency we're checking in the requirements.in file
|
24
|
+
# - Run `pip-compile` and see what the result is
|
25
|
+
class PipCompileVersionResolver
|
26
|
+
GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none --quiet (?<url>[^\s]+).* /
|
27
|
+
GIT_REFERENCE_NOT_FOUND_REGEX = /Did not find branch or tag '(?<tag>[^\n"]+)'/m
|
28
|
+
NATIVE_COMPILATION_ERROR =
|
29
|
+
"pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1"
|
30
|
+
# See https://packaging.python.org/en/latest/tutorials/packaging-projects/#configuring-metadata
|
31
|
+
PYTHON_PACKAGE_NAME_REGEX = /[A-Za-z0-9_\-]+/
|
32
|
+
RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible"
|
33
|
+
ERROR_REGEX = /(?<=ERROR\:\W).*$/
|
34
|
+
UV_UNRESOLVABLE_REGEX = / × No solution found when resolving dependencies:[\s\S]*$/
|
35
|
+
|
36
|
+
attr_reader :dependency
|
37
|
+
attr_reader :dependency_files
|
38
|
+
attr_reader :credentials
|
39
|
+
attr_reader :repo_contents_path
|
40
|
+
attr_reader :error_handler
|
41
|
+
|
42
|
+
def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:)
|
43
|
+
@dependency = dependency
|
44
|
+
@dependency_files = dependency_files
|
45
|
+
@credentials = credentials
|
46
|
+
@repo_contents_path = repo_contents_path
|
47
|
+
@build_isolation = true
|
48
|
+
@error_handler = PipCompileErrorHandler.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def latest_resolvable_version(requirement: nil)
|
52
|
+
@latest_resolvable_version_string ||= {}
|
53
|
+
return @latest_resolvable_version_string[requirement] if @latest_resolvable_version_string.key?(requirement)
|
54
|
+
|
55
|
+
version_string =
|
56
|
+
fetch_latest_resolvable_version_string(requirement: requirement)
|
57
|
+
|
58
|
+
@latest_resolvable_version_string[requirement] ||=
|
59
|
+
version_string.nil? ? nil : Uv::Version.new(version_string)
|
60
|
+
end
|
61
|
+
|
62
|
+
def resolvable?(version:)
|
63
|
+
@resolvable ||= {}
|
64
|
+
return @resolvable[version] if @resolvable.key?(version)
|
65
|
+
|
66
|
+
@resolvable[version] = if latest_resolvable_version(requirement: "==#{version}")
|
67
|
+
true
|
68
|
+
else
|
69
|
+
false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def fetch_latest_resolvable_version_string(requirement:)
|
76
|
+
SharedHelpers.in_a_temporary_directory do
|
77
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
78
|
+
write_temporary_dependency_files(updated_req: requirement)
|
79
|
+
language_version_manager.install_required_python
|
80
|
+
|
81
|
+
filenames_to_compile.each do |filename|
|
82
|
+
return nil unless compile_file(filename)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Remove any .python-version file before parsing the reqs
|
86
|
+
FileUtils.remove_entry(".python-version", true)
|
87
|
+
|
88
|
+
parse_updated_files
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def compile_file(filename)
|
94
|
+
# Shell out to pip-compile.
|
95
|
+
# This is slow, as pip-compile needs to do installs.
|
96
|
+
options = pip_compile_options(filename)
|
97
|
+
options_fingerprint = pip_compile_options_fingerprint(options)
|
98
|
+
|
99
|
+
run_pip_compile_command(
|
100
|
+
"pyenv exec uv pip compile -v #{options} -P #{dependency.name} #{filename}",
|
101
|
+
fingerprint: "pyenv exec uv pip compile -v #{options_fingerprint} -P <dependency_name> <filename>"
|
102
|
+
)
|
103
|
+
|
104
|
+
return true if dependency.top_level?
|
105
|
+
|
106
|
+
# Run pip-compile a second time for transient dependencies
|
107
|
+
# to make sure we do not update dependencies that are
|
108
|
+
# superfluous. pip-compile does not detect these when
|
109
|
+
# updating a specific dependency with the -P option.
|
110
|
+
# Running pip-compile a second time will automatically remove
|
111
|
+
# superfluous dependencies. Dependabot then marks those with
|
112
|
+
# update_not_possible.
|
113
|
+
write_original_manifest_files
|
114
|
+
run_pip_compile_command(
|
115
|
+
"pyenv exec uv pip compile #{options} #{filename}",
|
116
|
+
fingerprint: "pyenv exec uv pip compile #{options_fingerprint} <filename>"
|
117
|
+
)
|
118
|
+
|
119
|
+
true
|
120
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
121
|
+
retry_count ||= 0
|
122
|
+
retry_count += 1
|
123
|
+
if compilation_error?(e) && retry_count <= 1
|
124
|
+
@build_isolation = false
|
125
|
+
retry
|
126
|
+
end
|
127
|
+
|
128
|
+
handle_pip_compile_errors(e.message)
|
129
|
+
end
|
130
|
+
|
131
|
+
def compilation_error?(error)
|
132
|
+
error.message.include?(NATIVE_COMPILATION_ERROR)
|
133
|
+
end
|
134
|
+
|
135
|
+
# rubocop:disable Metrics/AbcSize
|
136
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
137
|
+
def handle_pip_compile_errors(message)
|
138
|
+
if message.include?("No solution found when resolving dependencies")
|
139
|
+
raise DependencyFileNotResolvable, message.scan(UV_UNRESOLVABLE_REGEX).last
|
140
|
+
end
|
141
|
+
|
142
|
+
check_original_requirements_resolvable if message.include?(RESOLUTION_IMPOSSIBLE_ERROR)
|
143
|
+
|
144
|
+
# If there's an unsupported constraint, check if it existed
|
145
|
+
# previously (and raise if it did)
|
146
|
+
check_original_requirements_resolvable if message.include?("UnsupportedConstraint")
|
147
|
+
|
148
|
+
if message.include?(RESOLUTION_IMPOSSIBLE_ERROR) &&
|
149
|
+
!message.match?(/#{Regexp.quote(dependency.name)}/i)
|
150
|
+
# Sometimes pip-tools gets confused and can't work around
|
151
|
+
# sub-dependency incompatibilities. Ignore those cases.
|
152
|
+
return nil
|
153
|
+
end
|
154
|
+
|
155
|
+
if message.match?(GIT_REFERENCE_NOT_FOUND_REGEX)
|
156
|
+
tag = message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag")
|
157
|
+
constraints_section = message.split("Finding the best candidates:").first
|
158
|
+
egg_regex = /#{Regexp.escape(tag)}#egg=(#{PYTHON_PACKAGE_NAME_REGEX})/
|
159
|
+
name_match = constraints_section.scan(egg_regex)
|
160
|
+
|
161
|
+
# We can determine the name of the package from another part of the logger output if it has a unique tag
|
162
|
+
raise GitDependencyReferenceNotFound, name_match.first.first if name_match.length == 1
|
163
|
+
|
164
|
+
raise GitDependencyReferenceNotFound, "(unknown package at #{tag})"
|
165
|
+
end
|
166
|
+
|
167
|
+
if message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX)
|
168
|
+
url = message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX)
|
169
|
+
.named_captures.fetch("url")
|
170
|
+
raise GitDependenciesNotReachable, url
|
171
|
+
end
|
172
|
+
|
173
|
+
raise Dependabot::OutOfDisk if message.end_with?("[Errno 28] No space left on device")
|
174
|
+
|
175
|
+
raise Dependabot::OutOfMemory if message.end_with?("MemoryError")
|
176
|
+
|
177
|
+
error_handler.handle_pipcompile_error(message)
|
178
|
+
|
179
|
+
raise
|
180
|
+
end
|
181
|
+
# rubocop:enable Metrics/AbcSize
|
182
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
183
|
+
|
184
|
+
# Needed because pip-compile's resolver isn't perfect.
|
185
|
+
# Note: We raise errors from this method, rather than returning a
|
186
|
+
# boolean, so that all deps for this repo will raise identical
|
187
|
+
# errors when failing to update
|
188
|
+
def check_original_requirements_resolvable
|
189
|
+
SharedHelpers.in_a_temporary_directory do
|
190
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
191
|
+
write_temporary_dependency_files(update_requirement: false)
|
192
|
+
|
193
|
+
filenames_to_compile.each do |filename|
|
194
|
+
options = pip_compile_options(filename)
|
195
|
+
options_fingerprint = pip_compile_options_fingerprint(options)
|
196
|
+
|
197
|
+
run_pip_compile_command(
|
198
|
+
"pyenv exec uv pip compile #{options} #{filename}",
|
199
|
+
fingerprint: "pyenv exec uv pip compile #{options_fingerprint} <filename>"
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
true
|
204
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
205
|
+
# Pick the error message that includes resolvability errors, this might be the cause from
|
206
|
+
# handle_pip_compile_errors (it's unclear if we should always pick the cause here)
|
207
|
+
error_message = [e.message, e.cause&.message].compact.find do |msg|
|
208
|
+
msg.include?(RESOLUTION_IMPOSSIBLE_ERROR)
|
209
|
+
end
|
210
|
+
|
211
|
+
cleaned_message = clean_error_message(error_message || "")
|
212
|
+
raise if cleaned_message.empty?
|
213
|
+
|
214
|
+
raise DependencyFileNotResolvable, cleaned_message
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def run_command(command, env: python_env, fingerprint:)
|
220
|
+
SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint, stderr_to_stdout: true)
|
221
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
222
|
+
handle_pip_compile_errors(e.message)
|
223
|
+
end
|
224
|
+
|
225
|
+
def pip_compile_options_fingerprint(options)
|
226
|
+
options.sub(
|
227
|
+
/--output-file=\S+/, "--output-file=<output_file>"
|
228
|
+
).sub(
|
229
|
+
/--index-url=\S+/, "--index-url=<index_url>"
|
230
|
+
).sub(
|
231
|
+
/--extra-index-url=\S+/, "--extra-index-url=<extra_index_url>"
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
def pip_compile_options(filename)
|
236
|
+
options = @build_isolation ? ["--build-isolation"] : ["--no-build-isolation"]
|
237
|
+
options += pip_compile_index_options
|
238
|
+
# TODO: Stop explicitly specifying `allow-unsafe` once it becomes the default:
|
239
|
+
# https://github.com/jazzband/pip-tools/issues/989#issuecomment-1661254701
|
240
|
+
options += ["--allow-unsafe"]
|
241
|
+
|
242
|
+
if (requirements_file = compiled_file_for_filename(filename))
|
243
|
+
options << "--output-file=#{requirements_file.name}"
|
244
|
+
options += uv_pip_compile_options_from_compiled_file(requirements_file)
|
245
|
+
end
|
246
|
+
|
247
|
+
options.join(" ")
|
248
|
+
end
|
249
|
+
|
250
|
+
def pip_compile_index_options
|
251
|
+
credentials
|
252
|
+
.select { |cred| cred["type"] == "python_index" }
|
253
|
+
.map do |cred|
|
254
|
+
authed_url = AuthedUrlBuilder.authed_url(credential: cred)
|
255
|
+
|
256
|
+
if cred.replaces_base?
|
257
|
+
"--index-url=#{authed_url}"
|
258
|
+
else
|
259
|
+
"--extra-index-url=#{authed_url}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def run_pip_compile_command(command, fingerprint:)
|
265
|
+
run_command(
|
266
|
+
"pyenv local #{language_version_manager.python_major_minor}",
|
267
|
+
fingerprint: "pyenv local <python_major_minor>"
|
268
|
+
)
|
269
|
+
|
270
|
+
run_command(command, fingerprint: fingerprint)
|
271
|
+
end
|
272
|
+
|
273
|
+
def uv_pip_compile_options_from_compiled_file(requirements_file)
|
274
|
+
options = []
|
275
|
+
|
276
|
+
options << "--no-emit-index-url" unless requirements_file.content.include?("index-url http")
|
277
|
+
|
278
|
+
options << "--generate-hashes" if requirements_file.content.include?("--hash=sha")
|
279
|
+
|
280
|
+
options << "--no-annotate" unless requirements_file.content.include?("# via ")
|
281
|
+
|
282
|
+
options << "--pre" if requirements_file.content.include?("--pre")
|
283
|
+
|
284
|
+
options << "--no-strip-extras" if requirements_file.content.include?("--no-strip-extras")
|
285
|
+
|
286
|
+
if requirements_file.content.include?("--no-binary") || requirements_file.content.include?("--only-binary")
|
287
|
+
options << "--emit-build-options"
|
288
|
+
end
|
289
|
+
|
290
|
+
if (resolver = FileUpdater::CompileFileUpdater::RESOLVER_REGEX.match(requirements_file.content))
|
291
|
+
options << "--resolver=#{resolver}"
|
292
|
+
end
|
293
|
+
|
294
|
+
options << "--universal" if requirements_file.content.include?("--universal")
|
295
|
+
|
296
|
+
options
|
297
|
+
end
|
298
|
+
|
299
|
+
def python_env
|
300
|
+
env = {}
|
301
|
+
|
302
|
+
# Handle Apache Airflow 1.10.x installs
|
303
|
+
if dependency_files.any? { |f| f.content.include?("apache-airflow") }
|
304
|
+
if dependency_files.any? { |f| f.content.include?("unidecode") }
|
305
|
+
env["AIRFLOW_GPL_UNIDECODE"] = "yes"
|
306
|
+
else
|
307
|
+
env["SLUGIFY_USES_TEXT_UNIDECODE"] = "yes"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
env
|
312
|
+
end
|
313
|
+
|
314
|
+
def write_temporary_dependency_files(updated_req: nil,
|
315
|
+
update_requirement: true)
|
316
|
+
dependency_files.each do |file|
|
317
|
+
path = file.name
|
318
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
319
|
+
updated_content =
|
320
|
+
if update_requirement then update_req_file(file, updated_req)
|
321
|
+
else
|
322
|
+
file.content
|
323
|
+
end
|
324
|
+
File.write(path, updated_content)
|
325
|
+
end
|
326
|
+
|
327
|
+
# Overwrite the .python-version with updated content
|
328
|
+
File.write(".python-version", language_version_manager.python_major_minor)
|
329
|
+
end
|
330
|
+
|
331
|
+
def write_original_manifest_files
|
332
|
+
pip_compile_files.each do |file|
|
333
|
+
FileUtils.mkdir_p(Pathname.new(file.name).dirname)
|
334
|
+
File.write(file.name, file.content)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def update_req_file(file, updated_req)
|
339
|
+
return file.content unless file.name.end_with?(".in")
|
340
|
+
|
341
|
+
req = dependency.requirements.find { |r| r[:file] == file.name }
|
342
|
+
|
343
|
+
return file.content + "\n#{dependency.name} #{updated_req}" unless req&.fetch(:requirement)
|
344
|
+
|
345
|
+
Uv::FileUpdater::RequirementReplacer.new(
|
346
|
+
content: file.content,
|
347
|
+
dependency_name: dependency.name,
|
348
|
+
old_requirement: req[:requirement],
|
349
|
+
new_requirement: updated_req
|
350
|
+
).updated_content
|
351
|
+
end
|
352
|
+
|
353
|
+
def normalise(name)
|
354
|
+
NameNormaliser.normalise(name)
|
355
|
+
end
|
356
|
+
|
357
|
+
def clean_error_message(message)
|
358
|
+
message.scan(ERROR_REGEX).last
|
359
|
+
end
|
360
|
+
|
361
|
+
def filenames_to_compile
|
362
|
+
files_from_reqs =
|
363
|
+
dependency.requirements
|
364
|
+
.map { |r| r[:file] }
|
365
|
+
.select { |fn| fn.end_with?(".in") }
|
366
|
+
|
367
|
+
files_from_compiled_files =
|
368
|
+
pip_compile_files.map(&:name).select do |fn|
|
369
|
+
compiled_file = compiled_file_for_filename(fn)
|
370
|
+
compiled_file_includes_dependency?(compiled_file)
|
371
|
+
end
|
372
|
+
|
373
|
+
filenames = [*files_from_reqs, *files_from_compiled_files].uniq
|
374
|
+
|
375
|
+
order_filenames_for_compilation(filenames)
|
376
|
+
end
|
377
|
+
|
378
|
+
def compiled_file_for_filename(filename)
|
379
|
+
compiled_file =
|
380
|
+
compiled_files
|
381
|
+
.find { |f| f.content.match?(output_file_regex(filename)) }
|
382
|
+
|
383
|
+
compiled_file ||=
|
384
|
+
compiled_files
|
385
|
+
.find { |f| f.name == filename.gsub(/\.in$/, ".txt") }
|
386
|
+
|
387
|
+
compiled_file
|
388
|
+
end
|
389
|
+
|
390
|
+
def output_file_regex(filename)
|
391
|
+
"--output-file[=\s]+.*\s#{Regexp.escape(filename)}\s*$"
|
392
|
+
end
|
393
|
+
|
394
|
+
def compiled_file_includes_dependency?(compiled_file)
|
395
|
+
return false unless compiled_file
|
396
|
+
|
397
|
+
regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
|
398
|
+
|
399
|
+
matches = []
|
400
|
+
compiled_file.content.scan(regex) { matches << Regexp.last_match }
|
401
|
+
matches.any? { |m| normalise(m[:name]) == dependency.name }
|
402
|
+
end
|
403
|
+
|
404
|
+
# If the files we need to update require one another then we need to
|
405
|
+
# update them in the right order
|
406
|
+
def order_filenames_for_compilation(filenames)
|
407
|
+
ordered_filenames = T.let([], T::Array[String])
|
408
|
+
|
409
|
+
while (remaining_filenames = filenames - ordered_filenames).any?
|
410
|
+
ordered_filenames +=
|
411
|
+
remaining_filenames
|
412
|
+
.reject do |fn|
|
413
|
+
unupdated_reqs = requirement_map[fn] - ordered_filenames
|
414
|
+
unupdated_reqs.intersect?(filenames)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
ordered_filenames
|
419
|
+
end
|
420
|
+
|
421
|
+
def requirement_map
|
422
|
+
child_req_regex = Uv::FileFetcher::CHILD_REQUIREMENT_REGEX
|
423
|
+
@requirement_map ||=
|
424
|
+
pip_compile_files.each_with_object({}) do |file, req_map|
|
425
|
+
paths = file.content.scan(child_req_regex).flatten
|
426
|
+
current_dir = File.dirname(file.name)
|
427
|
+
|
428
|
+
req_map[file.name] =
|
429
|
+
paths.map do |path|
|
430
|
+
path = File.join(current_dir, path) if current_dir != "."
|
431
|
+
path = Pathname.new(path).cleanpath.to_path
|
432
|
+
path = path.gsub(/\.txt$/, ".in")
|
433
|
+
next if path == file.name
|
434
|
+
|
435
|
+
path
|
436
|
+
end.uniq.compact
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def parse_updated_files
|
441
|
+
updated_files =
|
442
|
+
dependency_files.map do |file|
|
443
|
+
next file if file.name == ".python-version"
|
444
|
+
|
445
|
+
updated_file = file.dup
|
446
|
+
updated_file.content = File.read(file.name)
|
447
|
+
updated_file
|
448
|
+
end
|
449
|
+
|
450
|
+
Uv::FileParser.new(
|
451
|
+
dependency_files: updated_files,
|
452
|
+
source: nil,
|
453
|
+
credentials: credentials
|
454
|
+
).parse.find { |d| d.name == dependency.name }&.version
|
455
|
+
end
|
456
|
+
|
457
|
+
def python_requirement_parser
|
458
|
+
@python_requirement_parser ||=
|
459
|
+
FileParser::PythonRequirementParser.new(
|
460
|
+
dependency_files: dependency_files
|
461
|
+
)
|
462
|
+
end
|
463
|
+
|
464
|
+
def language_version_manager
|
465
|
+
@language_version_manager ||=
|
466
|
+
LanguageVersionManager.new(
|
467
|
+
python_requirement_parser: python_requirement_parser
|
468
|
+
)
|
469
|
+
end
|
470
|
+
|
471
|
+
def setup_files
|
472
|
+
dependency_files.select { |f| f.name.end_with?("setup.py") }
|
473
|
+
end
|
474
|
+
|
475
|
+
def pip_compile_files
|
476
|
+
dependency_files.select { |f| f.name.end_with?(".in") }
|
477
|
+
end
|
478
|
+
|
479
|
+
def compiled_files
|
480
|
+
dependency_files.select { |f| f.name.end_with?(".txt") }
|
481
|
+
end
|
482
|
+
|
483
|
+
def setup_cfg_files
|
484
|
+
dependency_files.select { |f| f.name.end_with?("setup.cfg") }
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
class PipCompileErrorHandler
|
490
|
+
SUBPROCESS_ERROR = /subprocess-exited-with-error/
|
491
|
+
|
492
|
+
INSTALLATION_ERROR = /InstallationError/
|
493
|
+
|
494
|
+
INSTALLATION_SUBPROCESS_ERROR = /InstallationSubprocessError/
|
495
|
+
|
496
|
+
HASH_MISMATCH = /HashMismatch/
|
497
|
+
|
498
|
+
def handle_pipcompile_error(error)
|
499
|
+
return unless error.match?(SUBPROCESS_ERROR) || error.match?(INSTALLATION_ERROR) ||
|
500
|
+
error.match?(INSTALLATION_SUBPROCESS_ERROR) || error.match?(HASH_MISMATCH)
|
501
|
+
|
502
|
+
raise DependencyFileNotResolvable, "Error resolving dependency"
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/uv/language_version_manager"
|
5
|
+
require "dependabot/uv/update_checker"
|
6
|
+
require "dependabot/uv/update_checker/latest_version_finder"
|
7
|
+
require "dependabot/uv/file_parser/python_requirement_parser"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Uv
|
11
|
+
class UpdateChecker
|
12
|
+
class PipVersionResolver
|
13
|
+
def initialize(dependency:, dependency_files:, credentials:,
|
14
|
+
ignored_versions:, raise_on_ignored: false,
|
15
|
+
security_advisories:)
|
16
|
+
@dependency = dependency
|
17
|
+
@dependency_files = dependency_files
|
18
|
+
@credentials = credentials
|
19
|
+
@ignored_versions = ignored_versions
|
20
|
+
@raise_on_ignored = raise_on_ignored
|
21
|
+
@security_advisories = security_advisories
|
22
|
+
end
|
23
|
+
|
24
|
+
def latest_resolvable_version
|
25
|
+
latest_version_finder.latest_version(python_version: language_version_manager.python_version)
|
26
|
+
end
|
27
|
+
|
28
|
+
def latest_resolvable_version_with_no_unlock
|
29
|
+
latest_version_finder
|
30
|
+
.latest_version_with_no_unlock(python_version: language_version_manager.python_version)
|
31
|
+
end
|
32
|
+
|
33
|
+
def lowest_resolvable_security_fix_version
|
34
|
+
latest_version_finder
|
35
|
+
.lowest_security_fix_version(python_version: language_version_manager.python_version)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :dependency
|
41
|
+
attr_reader :dependency_files
|
42
|
+
attr_reader :credentials
|
43
|
+
attr_reader :ignored_versions
|
44
|
+
attr_reader :security_advisories
|
45
|
+
|
46
|
+
def latest_version_finder
|
47
|
+
@latest_version_finder ||= LatestVersionFinder.new(
|
48
|
+
dependency: dependency,
|
49
|
+
dependency_files: dependency_files,
|
50
|
+
credentials: credentials,
|
51
|
+
ignored_versions: ignored_versions,
|
52
|
+
raise_on_ignored: @raise_on_ignored,
|
53
|
+
security_advisories: security_advisories
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def python_requirement_parser
|
58
|
+
@python_requirement_parser ||=
|
59
|
+
FileParser::PythonRequirementParser.new(
|
60
|
+
dependency_files: dependency_files
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def language_version_manager
|
65
|
+
@language_version_manager ||=
|
66
|
+
LanguageVersionManager.new(
|
67
|
+
python_requirement_parser: python_requirement_parser
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|