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,73 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/uv/requirement_parser"
|
5
|
+
require "dependabot/uv/file_updater"
|
6
|
+
require "dependabot/shared_helpers"
|
7
|
+
require "dependabot/uv/native_helpers"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Uv
|
11
|
+
class FileUpdater
|
12
|
+
class RequirementFileUpdater
|
13
|
+
require_relative "requirement_replacer"
|
14
|
+
|
15
|
+
attr_reader :dependencies
|
16
|
+
attr_reader :dependency_files
|
17
|
+
attr_reader :credentials
|
18
|
+
|
19
|
+
def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
|
20
|
+
@dependencies = dependencies
|
21
|
+
@dependency_files = dependency_files
|
22
|
+
@credentials = credentials
|
23
|
+
@index_urls = index_urls
|
24
|
+
end
|
25
|
+
|
26
|
+
def updated_dependency_files
|
27
|
+
@updated_dependency_files ||= fetch_updated_dependency_files
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def dependency
|
33
|
+
# For now, we'll only ever be updating a single dependency
|
34
|
+
dependencies.first
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_updated_dependency_files
|
38
|
+
reqs = dependency.requirements.zip(dependency.previous_requirements)
|
39
|
+
|
40
|
+
reqs.filter_map do |(new_req, old_req)|
|
41
|
+
next if new_req == old_req
|
42
|
+
|
43
|
+
file = get_original_file(new_req.fetch(:file)).dup
|
44
|
+
updated_content =
|
45
|
+
updated_requirement_or_setup_file_content(new_req, old_req)
|
46
|
+
next if updated_content == file.content
|
47
|
+
|
48
|
+
file.content = updated_content
|
49
|
+
file
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def updated_requirement_or_setup_file_content(new_req, old_req)
|
54
|
+
original_file = get_original_file(new_req.fetch(:file))
|
55
|
+
raise "Could not find a dependency file for #{new_req}" unless original_file
|
56
|
+
|
57
|
+
RequirementReplacer.new(
|
58
|
+
content: original_file.content,
|
59
|
+
dependency_name: dependency.name,
|
60
|
+
old_requirement: old_req.fetch(:requirement),
|
61
|
+
new_requirement: new_req.fetch(:requirement),
|
62
|
+
new_hash_version: dependency.version,
|
63
|
+
index_urls: @index_urls
|
64
|
+
).updated_content
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_original_file(filename)
|
68
|
+
dependency_files.find { |f| f.name == filename }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/dependency"
|
5
|
+
require "dependabot/uv/requirement_parser"
|
6
|
+
require "dependabot/uv/file_updater"
|
7
|
+
require "dependabot/shared_helpers"
|
8
|
+
require "dependabot/uv/native_helpers"
|
9
|
+
require "dependabot/uv/name_normaliser"
|
10
|
+
|
11
|
+
module Dependabot
|
12
|
+
module Uv
|
13
|
+
class FileUpdater
|
14
|
+
class RequirementReplacer
|
15
|
+
PACKAGE_NOT_FOUND_ERROR = "PackageNotFoundError"
|
16
|
+
|
17
|
+
CERTIFICATE_VERIFY_FAILED = /CERTIFICATE_VERIFY_FAILED/
|
18
|
+
|
19
|
+
def initialize(content:, dependency_name:, old_requirement:,
|
20
|
+
new_requirement:, new_hash_version: nil, index_urls: nil)
|
21
|
+
@content = content
|
22
|
+
@dependency_name = normalise(dependency_name)
|
23
|
+
@old_requirement = old_requirement
|
24
|
+
@new_requirement = new_requirement
|
25
|
+
@new_hash_version = new_hash_version
|
26
|
+
@index_urls = index_urls
|
27
|
+
end
|
28
|
+
|
29
|
+
def updated_content
|
30
|
+
updated_content =
|
31
|
+
content.gsub(original_declaration_replacement_regex) do |mtch|
|
32
|
+
# If the "declaration" is setting an option (e.g., no-binary)
|
33
|
+
# ignore it, since it isn't actually a declaration
|
34
|
+
next mtch if Regexp.last_match&.pre_match&.match?(/--.*\z/)
|
35
|
+
|
36
|
+
updated_dependency_declaration_string
|
37
|
+
end
|
38
|
+
|
39
|
+
raise "Expected content to change!" if old_requirement != new_requirement && content == updated_content
|
40
|
+
|
41
|
+
updated_content
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_reader :content
|
47
|
+
attr_reader :dependency_name
|
48
|
+
attr_reader :old_requirement
|
49
|
+
attr_reader :new_requirement
|
50
|
+
attr_reader :new_hash_version
|
51
|
+
|
52
|
+
def update_hashes?
|
53
|
+
!new_hash_version.nil?
|
54
|
+
end
|
55
|
+
|
56
|
+
def updated_requirement_string
|
57
|
+
new_req_string = new_requirement
|
58
|
+
|
59
|
+
new_req_string = new_req_string.gsub(/,\s*/, ", ") if add_space_after_commas?
|
60
|
+
|
61
|
+
if add_space_after_operators?
|
62
|
+
new_req_string =
|
63
|
+
new_req_string
|
64
|
+
.gsub(/(#{RequirementParser::COMPARISON})\s*(?=\d)/o, '\1 ')
|
65
|
+
end
|
66
|
+
|
67
|
+
new_req_string
|
68
|
+
end
|
69
|
+
|
70
|
+
def updated_dependency_declaration_string
|
71
|
+
old_req = old_requirement
|
72
|
+
updated_string =
|
73
|
+
if old_req
|
74
|
+
original_dependency_declaration_string(old_req)
|
75
|
+
.sub(RequirementParser::REQUIREMENTS, updated_requirement_string)
|
76
|
+
else
|
77
|
+
original_dependency_declaration_string(old_req)
|
78
|
+
.sub(RequirementParser::NAME_WITH_EXTRAS) do |nm|
|
79
|
+
nm + updated_requirement_string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
return updated_string unless update_hashes? && requirement_includes_hashes?(old_req)
|
84
|
+
|
85
|
+
updated_string.sub(
|
86
|
+
RequirementParser::HASHES,
|
87
|
+
package_hashes_for(
|
88
|
+
name: dependency_name,
|
89
|
+
version: new_hash_version,
|
90
|
+
algorithm: hash_algorithm(old_req)
|
91
|
+
).join(hash_separator(old_req))
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_space_after_commas?
|
96
|
+
original_dependency_declaration_string(old_requirement)
|
97
|
+
.match(RequirementParser::REQUIREMENTS)
|
98
|
+
.to_s.include?(", ")
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_space_after_operators?
|
102
|
+
original_dependency_declaration_string(old_requirement)
|
103
|
+
.match(RequirementParser::REQUIREMENTS)
|
104
|
+
.to_s.match?(/#{RequirementParser::COMPARISON}\s+\d/o)
|
105
|
+
end
|
106
|
+
|
107
|
+
def original_declaration_replacement_regex
|
108
|
+
original_string =
|
109
|
+
original_dependency_declaration_string(old_requirement)
|
110
|
+
/(?<![\-\w\.\[])#{Regexp.escape(original_string)}(?![\-\w\.])/
|
111
|
+
end
|
112
|
+
|
113
|
+
def requirement_includes_hashes?(requirement)
|
114
|
+
original_dependency_declaration_string(requirement)
|
115
|
+
.match?(RequirementParser::HASHES)
|
116
|
+
end
|
117
|
+
|
118
|
+
def hash_algorithm(requirement)
|
119
|
+
return unless requirement_includes_hashes?(requirement)
|
120
|
+
|
121
|
+
original_dependency_declaration_string(requirement)
|
122
|
+
.match(RequirementParser::HASHES)
|
123
|
+
.named_captures.fetch("algorithm")
|
124
|
+
end
|
125
|
+
|
126
|
+
def hash_separator(requirement)
|
127
|
+
return unless requirement_includes_hashes?(requirement)
|
128
|
+
|
129
|
+
hash_regex = RequirementParser::HASH
|
130
|
+
current_separator =
|
131
|
+
original_dependency_declaration_string(requirement)
|
132
|
+
.match(/#{hash_regex}((?<separator>\s*\\?\s*?)#{hash_regex})*/)
|
133
|
+
.named_captures.fetch("separator")
|
134
|
+
|
135
|
+
default_separator =
|
136
|
+
original_dependency_declaration_string(requirement)
|
137
|
+
.match(RequirementParser::HASH)
|
138
|
+
.pre_match.match(/(?<separator>\s*\\?\s*?)\z/)
|
139
|
+
.named_captures.fetch("separator")
|
140
|
+
|
141
|
+
current_separator || default_separator
|
142
|
+
end
|
143
|
+
|
144
|
+
def package_hashes_for(name:, version:, algorithm:)
|
145
|
+
index_urls = @index_urls || [nil]
|
146
|
+
|
147
|
+
index_urls.map do |index_url|
|
148
|
+
args = [name, version, algorithm]
|
149
|
+
args << index_url unless index_url.nil?
|
150
|
+
|
151
|
+
begin
|
152
|
+
result = SharedHelpers.run_helper_subprocess(
|
153
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
154
|
+
function: "get_dependency_hash",
|
155
|
+
args: args
|
156
|
+
)
|
157
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
158
|
+
requirement_error_handler(e)
|
159
|
+
|
160
|
+
raise unless e.message.include?("PackageNotFoundError")
|
161
|
+
|
162
|
+
next
|
163
|
+
end
|
164
|
+
|
165
|
+
return result.map { |h| "--hash=#{algorithm}:#{h['hash']}" } if result.is_a?(Array)
|
166
|
+
end
|
167
|
+
|
168
|
+
raise Dependabot::DependencyFileNotResolvable, "Unable to find hashes for package #{name}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def original_dependency_declaration_string(old_req)
|
172
|
+
matches = []
|
173
|
+
|
174
|
+
dec =
|
175
|
+
if old_req.nil?
|
176
|
+
regex = RequirementParser::INSTALL_REQ_WITHOUT_REQUIREMENT
|
177
|
+
content.scan(regex) { matches << Regexp.last_match }
|
178
|
+
matches.find { |m| normalise(m[:name]) == dependency_name }
|
179
|
+
else
|
180
|
+
regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
|
181
|
+
content.scan(regex) { matches << Regexp.last_match }
|
182
|
+
matches
|
183
|
+
.select { |m| normalise(m[:name]) == dependency_name }
|
184
|
+
.find { |m| requirements_match(m[:requirements], old_req) }
|
185
|
+
end
|
186
|
+
|
187
|
+
raise "Declaration not found for #{dependency_name}!" unless dec
|
188
|
+
|
189
|
+
dec.to_s.strip
|
190
|
+
end
|
191
|
+
|
192
|
+
def normalise(name)
|
193
|
+
NameNormaliser.normalise(name)
|
194
|
+
end
|
195
|
+
|
196
|
+
def requirements_match(req1, req2)
|
197
|
+
req1&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort ==
|
198
|
+
req2&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort
|
199
|
+
end
|
200
|
+
|
201
|
+
public
|
202
|
+
|
203
|
+
def requirement_error_handler(error)
|
204
|
+
Dependabot.logger.warn(error.message)
|
205
|
+
|
206
|
+
return unless error.message.match?(CERTIFICATE_VERIFY_FAILED)
|
207
|
+
|
208
|
+
msg = "Error resolving dependency."
|
209
|
+
raise DependencyFileNotResolvable, msg
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "toml-rb"
|
5
|
+
require "dependabot/file_updaters"
|
6
|
+
require "dependabot/file_updaters/base"
|
7
|
+
require "dependabot/shared_helpers"
|
8
|
+
require "sorbet-runtime"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Uv
|
12
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
require_relative "file_updater/compile_file_updater"
|
16
|
+
require_relative "file_updater/requirement_file_updater"
|
17
|
+
|
18
|
+
sig { override.returns(T::Array[Regexp]) }
|
19
|
+
def self.updated_files_regex
|
20
|
+
[
|
21
|
+
/^.*\.txt$/, # Match any .txt files (e.g., requirements.txt) at any level
|
22
|
+
/^.*\.in$/, # Match any .in files at any level
|
23
|
+
/^.*pyproject\.toml$/ # Match pyproject.toml at any level
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
sig { override.returns(T::Array[DependencyFile]) }
|
28
|
+
def updated_dependency_files
|
29
|
+
updated_files = updated_pip_compile_based_files
|
30
|
+
|
31
|
+
if updated_files.none? ||
|
32
|
+
updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
|
33
|
+
raise "No files have changed!"
|
34
|
+
end
|
35
|
+
|
36
|
+
updated_files
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
sig { returns(T.nilable(Symbol)) }
|
42
|
+
def subdependency_resolver
|
43
|
+
raise "Claimed to be a sub-dependency, but no lockfile exists!" if pip_compile_files.empty?
|
44
|
+
|
45
|
+
:pip_compile if pip_compile_files.any?
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { returns(T::Array[DependencyFile]) }
|
49
|
+
def updated_pip_compile_based_files
|
50
|
+
CompileFileUpdater.new(
|
51
|
+
dependencies: dependencies,
|
52
|
+
dependency_files: dependency_files,
|
53
|
+
credentials: credentials,
|
54
|
+
index_urls: pip_compile_index_urls
|
55
|
+
).updated_dependency_files
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { returns(T::Array[DependencyFile]) }
|
59
|
+
def updated_requirement_based_files
|
60
|
+
RequirementFileUpdater.new(
|
61
|
+
dependencies: dependencies,
|
62
|
+
dependency_files: dependency_files,
|
63
|
+
credentials: credentials,
|
64
|
+
index_urls: pip_compile_index_urls
|
65
|
+
).updated_dependency_files
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { returns(T::Array[String]) }
|
69
|
+
def pip_compile_index_urls
|
70
|
+
if credentials.any?(&:replaces_base?)
|
71
|
+
credentials.select(&:replaces_base?).map { |cred| AuthedUrlBuilder.authed_url(credential: cred) }
|
72
|
+
else
|
73
|
+
urls = credentials.map { |cred| AuthedUrlBuilder.authed_url(credential: cred) }
|
74
|
+
# If there are no credentials that replace the base, we need to
|
75
|
+
# ensure that the base URL is included in the list of extra-index-urls.
|
76
|
+
[nil, *urls]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { override.void }
|
81
|
+
def check_required_files
|
82
|
+
filenames = dependency_files.map(&:name)
|
83
|
+
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
|
84
|
+
return if pyproject
|
85
|
+
|
86
|
+
raise "Missing required files!"
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
90
|
+
def pyproject
|
91
|
+
@pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile))
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { returns(T::Array[DependencyFile]) }
|
95
|
+
def pip_compile_files
|
96
|
+
@pip_compile_files ||= T.let(
|
97
|
+
dependency_files.select { |f| f.name.end_with?(".in") },
|
98
|
+
T.nilable(T::Array[DependencyFile])
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
Dependabot::FileUpdaters.register("uv", Dependabot::Uv::FileUpdater)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
require "dependabot/uv/version"
|
6
|
+
require "dependabot/ecosystem"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Uv
|
10
|
+
LANGUAGE = "python"
|
11
|
+
|
12
|
+
class Language < Dependabot::Ecosystem::VersionManager
|
13
|
+
extend T::Sig
|
14
|
+
# These versions should match the versions specified at the top of `python/Dockerfile`
|
15
|
+
PYTHON_3_13 = "3.13"
|
16
|
+
PYTHON_3_12 = "3.12"
|
17
|
+
PYTHON_3_11 = "3.11"
|
18
|
+
PYTHON_3_10 = "3.10"
|
19
|
+
PYTHON_3_9 = "3.9"
|
20
|
+
PYTHON_3_8 = "3.8"
|
21
|
+
|
22
|
+
DEPRECATED_VERSIONS = T.let([Version.new(PYTHON_3_8)].freeze, T::Array[Dependabot::Version])
|
23
|
+
|
24
|
+
# Keep versions in ascending order
|
25
|
+
SUPPORTED_VERSIONS = T.let([
|
26
|
+
Version.new(PYTHON_3_9),
|
27
|
+
Version.new(PYTHON_3_10),
|
28
|
+
Version.new(PYTHON_3_11),
|
29
|
+
Version.new(PYTHON_3_12),
|
30
|
+
Version.new(PYTHON_3_13)
|
31
|
+
].freeze, T::Array[Dependabot::Version])
|
32
|
+
|
33
|
+
sig do
|
34
|
+
params(
|
35
|
+
detected_version: String,
|
36
|
+
raw_version: T.nilable(String),
|
37
|
+
requirement: T.nilable(Requirement)
|
38
|
+
).void
|
39
|
+
end
|
40
|
+
def initialize(detected_version:, raw_version: nil, requirement: nil)
|
41
|
+
super(
|
42
|
+
name: LANGUAGE,
|
43
|
+
detected_version: major_minor_version(detected_version),
|
44
|
+
version: raw_version ? Version.new(raw_version) : nil,
|
45
|
+
deprecated_versions: DEPRECATED_VERSIONS,
|
46
|
+
supported_versions: SUPPORTED_VERSIONS,
|
47
|
+
requirement: requirement,
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { override.returns(T::Boolean) }
|
52
|
+
def deprecated?
|
53
|
+
return false unless detected_version
|
54
|
+
return false if unsupported?
|
55
|
+
|
56
|
+
deprecated_versions.include?(detected_version)
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { override.returns(T::Boolean) }
|
60
|
+
def unsupported?
|
61
|
+
return false unless detected_version
|
62
|
+
|
63
|
+
supported_versions.all? { |supported| supported > detected_version }
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
sig { params(version: String).returns(Dependabot::Uv::Version) }
|
69
|
+
def major_minor_version(version)
|
70
|
+
major_minor = T.let(T.must(Version.new(version).segments[0..1]&.join(".")), String)
|
71
|
+
|
72
|
+
Version.new(major_minor)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/logger"
|
5
|
+
require "dependabot/uv/version"
|
6
|
+
require "sorbet-runtime"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Uv
|
10
|
+
class LanguageVersionManager
|
11
|
+
extend T::Sig
|
12
|
+
# This list must match the versions specified at the top of `python/Dockerfile`
|
13
|
+
PRE_INSTALLED_PYTHON_VERSIONS = %w(
|
14
|
+
3.13.2
|
15
|
+
3.12.9
|
16
|
+
3.11.11
|
17
|
+
3.10.16
|
18
|
+
3.9.21
|
19
|
+
).freeze
|
20
|
+
|
21
|
+
sig { params(python_requirement_parser: T.untyped).void }
|
22
|
+
def initialize(python_requirement_parser:)
|
23
|
+
@python_requirement_parser = python_requirement_parser
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { returns(T.nilable(String)) }
|
27
|
+
def install_required_python
|
28
|
+
# The leading space is important in the version check
|
29
|
+
return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")
|
30
|
+
|
31
|
+
SharedHelpers.run_shell_command(
|
32
|
+
"tar -axf /usr/local/.pyenv/versions/#{python_version}.tar.zst -C /usr/local/.pyenv/versions"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { returns(String) }
|
37
|
+
def installed_version
|
38
|
+
# Use `pyenv exec` to query the active Python version
|
39
|
+
output, _status = SharedHelpers.run_shell_command("pyenv exec python --version")
|
40
|
+
version = output.strip.split.last # Extract the version number (e.g., "3.13.1")
|
41
|
+
|
42
|
+
T.must(version)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { returns(T.untyped) }
|
46
|
+
def python_major_minor
|
47
|
+
@python_major_minor ||= T.let(T.must(Uv::Version.new(python_version).segments[0..1]).join("."), T.untyped)
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(String) }
|
51
|
+
def python_version
|
52
|
+
@python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { returns(String) }
|
56
|
+
def python_requirement_string
|
57
|
+
if user_specified_python_version
|
58
|
+
if user_specified_python_version.start_with?(/\d/)
|
59
|
+
parts = user_specified_python_version.split(".")
|
60
|
+
parts.fill("*", (parts.length)..2).join(".")
|
61
|
+
else
|
62
|
+
user_specified_python_version
|
63
|
+
end
|
64
|
+
else
|
65
|
+
python_version_matching_imputed_requirements || PRE_INSTALLED_PYTHON_VERSIONS.first
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def python_version_from_supported_versions
|
71
|
+
requirement_string = python_requirement_string
|
72
|
+
|
73
|
+
# If the requirement string isn't already a range (eg ">3.10"), coerce it to "major.minor.*".
|
74
|
+
# The patch version is ignored because a non-matching patch version is unlikely to affect resolution.
|
75
|
+
requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if requirement_string.start_with?(/\d/)
|
76
|
+
|
77
|
+
# Try to match one of our pre-installed Python versions
|
78
|
+
requirement = T.must(Uv::Requirement.requirements_array(requirement_string).first)
|
79
|
+
version = PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(Uv::Version.new(v)) }
|
80
|
+
return version if version
|
81
|
+
|
82
|
+
# Otherwise we have to raise
|
83
|
+
supported_versions = PRE_INSTALLED_PYTHON_VERSIONS.map { |x| x.gsub(/\.\d+$/, ".*") }.join(", ")
|
84
|
+
raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { returns(T.untyped) }
|
88
|
+
def user_specified_python_version
|
89
|
+
@python_requirement_parser.user_specified_requirements.first
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { returns(T.nilable(String)) }
|
93
|
+
def python_version_matching_imputed_requirements
|
94
|
+
compiled_file_python_requirement_markers =
|
95
|
+
@python_requirement_parser.imputed_requirements.map do |r|
|
96
|
+
Dependabot::Uv::Requirement.new(r)
|
97
|
+
end
|
98
|
+
python_version_matching(compiled_file_python_requirement_markers)
|
99
|
+
end
|
100
|
+
|
101
|
+
sig { params(requirements: T.untyped).returns(T.nilable(String)) }
|
102
|
+
def python_version_matching(requirements)
|
103
|
+
PRE_INSTALLED_PYTHON_VERSIONS.find do |version_string|
|
104
|
+
version = Uv::Version.new(version_string)
|
105
|
+
requirements.all? do |req|
|
106
|
+
next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
|
107
|
+
|
108
|
+
req.satisfied_by?(version)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|