dependabot-uv 0.356.0 → 0.358.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/lib/dependabot/uv/file_fetcher/workspace_fetcher.rb +126 -0
- data/lib/dependabot/uv/file_fetcher.rb +139 -0
- data/lib/dependabot/uv/file_updater/lock_file_updater.rb +132 -34
- data/lib/dependabot/uv/file_updater/version_config_parser.rb +172 -0
- data/lib/dependabot/uv/file_updater.rb +2 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b61cf200acbd26b1c5b680d31b17cee5ef1fa9a80b79d7be51907f325fe32b75
|
|
4
|
+
data.tar.gz: 5cd07c0084c8d303ffa2354338b378ae0fd180d89a0970b52b37d66d9b8775c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0eb042e953613765d8d4192dd9867167f47bcc70e8299a761799a98b34e321cc0e6159ee723372adee219daf6dc9c4fcac05203bc1386611e1be4f58d052fe46
|
|
7
|
+
data.tar.gz: d4e86f346d77bca9ebc40a729cab00822647880b4ffe2c18eadeb9a6be7e064b9b50aca14baf69e1c8cb13c0aec30598c5eb5ce1b7e1de084c779dfab242c150
|
|
@@ -40,6 +40,30 @@ module Dependabot
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
44
|
+
def version_source_files
|
|
45
|
+
return [] unless @pyproject
|
|
46
|
+
|
|
47
|
+
workspace_member_paths.flat_map do |member_path|
|
|
48
|
+
member_pyproject = fetch_workspace_member_pyproject(member_path)
|
|
49
|
+
fetch_version_source_files_for(member_path, member_pyproject)
|
|
50
|
+
rescue Dependabot::DependencyFileNotFound
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
56
|
+
def license_files
|
|
57
|
+
return [] unless @pyproject
|
|
58
|
+
|
|
59
|
+
workspace_member_paths.flat_map do |member_path|
|
|
60
|
+
member_pyproject = fetch_workspace_member_pyproject(member_path)
|
|
61
|
+
fetch_license_files_for(member_path, member_pyproject)
|
|
62
|
+
rescue Dependabot::DependencyFileNotFound
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
43
67
|
sig { returns(T::Array[{ name: String, file: String }]) }
|
|
44
68
|
def uv_sources_workspace_dependencies
|
|
45
69
|
return [] unless @pyproject
|
|
@@ -120,6 +144,108 @@ module Dependabot
|
|
|
120
144
|
end
|
|
121
145
|
end
|
|
122
146
|
|
|
147
|
+
sig do
|
|
148
|
+
params(
|
|
149
|
+
path: String,
|
|
150
|
+
pyproject_file: Dependabot::DependencyFile
|
|
151
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
152
|
+
end
|
|
153
|
+
def fetch_version_source_files_for(path, pyproject_file)
|
|
154
|
+
version_paths = extract_version_source_paths(pyproject_file)
|
|
155
|
+
|
|
156
|
+
version_paths.filter_map do |version_path|
|
|
157
|
+
resolved_path = resolve_support_file_path(version_path, path)
|
|
158
|
+
next unless resolved_path
|
|
159
|
+
next unless path_within_repo?(resolved_path)
|
|
160
|
+
|
|
161
|
+
file = fetch_file_from_host(resolved_path, fetch_submodules: true)
|
|
162
|
+
|
|
163
|
+
file.support_file = true
|
|
164
|
+
file
|
|
165
|
+
rescue Dependabot::DependencyFileNotFound
|
|
166
|
+
Dependabot.logger.info("Version source file not found: #{resolved_path}")
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
sig do
|
|
172
|
+
params(
|
|
173
|
+
path: String,
|
|
174
|
+
pyproject_file: Dependabot::DependencyFile
|
|
175
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
176
|
+
end
|
|
177
|
+
def fetch_license_files_for(path, pyproject_file)
|
|
178
|
+
license_paths = extract_license_paths(pyproject_file)
|
|
179
|
+
|
|
180
|
+
license_paths.filter_map do |license_path|
|
|
181
|
+
resolved_path = resolve_support_file_path(license_path, path)
|
|
182
|
+
next unless resolved_path
|
|
183
|
+
next unless path_within_repo?(resolved_path)
|
|
184
|
+
|
|
185
|
+
file = fetch_file_from_host(resolved_path, fetch_submodules: true)
|
|
186
|
+
|
|
187
|
+
file.support_file = true
|
|
188
|
+
file
|
|
189
|
+
rescue Dependabot::DependencyFileNotFound
|
|
190
|
+
Dependabot.logger.info("License file not found: #{resolved_path}")
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
|
196
|
+
def extract_version_source_paths(pyproject_file)
|
|
197
|
+
return [] unless pyproject_file.content
|
|
198
|
+
|
|
199
|
+
parsed = TomlRB.parse(T.must(pyproject_file.content))
|
|
200
|
+
paths = []
|
|
201
|
+
|
|
202
|
+
hatch_version_path = parsed.dig("tool", "hatch", "version", "path")
|
|
203
|
+
paths << hatch_version_path if hatch_version_path.is_a?(String)
|
|
204
|
+
|
|
205
|
+
paths
|
|
206
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
207
|
+
[]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
|
211
|
+
def extract_license_paths(pyproject_file)
|
|
212
|
+
return [] unless pyproject_file.content
|
|
213
|
+
|
|
214
|
+
parsed = TomlRB.parse(T.must(pyproject_file.content))
|
|
215
|
+
paths = []
|
|
216
|
+
|
|
217
|
+
# Handle legacy license = {file = "LICENSE"} format
|
|
218
|
+
license_decl = parsed.dig("project", "license")
|
|
219
|
+
paths << license_decl["file"] if license_decl.is_a?(Hash) && license_decl["file"].is_a?(String)
|
|
220
|
+
|
|
221
|
+
# Handle license-files = ["LICENSE", "LICENSES/*"] format (without glob expansion)
|
|
222
|
+
license_files_decl = parsed.dig("project", "license-files")
|
|
223
|
+
if license_files_decl.is_a?(Array)
|
|
224
|
+
license_files_decl.each do |pattern|
|
|
225
|
+
# Only include simple file paths, not glob patterns
|
|
226
|
+
paths << pattern if pattern.is_a?(String) && !pattern.include?("*")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
paths
|
|
231
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
232
|
+
[]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
sig { params(file_path: String, base_path: String).returns(T.nilable(String)) }
|
|
236
|
+
def resolve_support_file_path(file_path, base_path)
|
|
237
|
+
return nil if file_path.empty?
|
|
238
|
+
return nil if Pathname.new(file_path).absolute?
|
|
239
|
+
|
|
240
|
+
clean_path(File.join(base_path, file_path))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
244
|
+
def path_within_repo?(path)
|
|
245
|
+
cleaned = clean_path(path)
|
|
246
|
+
!cleaned.start_with?("../", "/")
|
|
247
|
+
end
|
|
248
|
+
|
|
123
249
|
sig { returns(T::Array[String]) }
|
|
124
250
|
def workspace_member_paths
|
|
125
251
|
return [] unless @pyproject
|
|
@@ -49,8 +49,10 @@ module Dependabot
|
|
|
49
49
|
def ecosystem_specific_files
|
|
50
50
|
files = []
|
|
51
51
|
files += readme_files
|
|
52
|
+
files += license_files
|
|
52
53
|
files += uv_lock_files
|
|
53
54
|
files += workspace_member_files
|
|
55
|
+
files += version_source_files
|
|
54
56
|
files
|
|
55
57
|
end
|
|
56
58
|
|
|
@@ -111,6 +113,143 @@ module Dependabot
|
|
|
111
113
|
workspace_fetcher.send(:fetch_readme_files_for, directory, T.must(pyproject))
|
|
112
114
|
end
|
|
113
115
|
|
|
116
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
117
|
+
def license_files
|
|
118
|
+
return [] unless pyproject
|
|
119
|
+
|
|
120
|
+
files = []
|
|
121
|
+
files += fetch_license_files_for(directory, T.must(pyproject))
|
|
122
|
+
files += workspace_fetcher.license_files
|
|
123
|
+
files
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
sig do
|
|
127
|
+
params(
|
|
128
|
+
base_path: String,
|
|
129
|
+
pyproject_file: Dependabot::DependencyFile
|
|
130
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
131
|
+
end
|
|
132
|
+
def fetch_license_files_for(base_path, pyproject_file)
|
|
133
|
+
license_paths = extract_license_paths(pyproject_file)
|
|
134
|
+
is_root = base_path == directory
|
|
135
|
+
|
|
136
|
+
license_paths.filter_map do |license_path|
|
|
137
|
+
resolved_path = resolve_support_file_path(license_path, base_path)
|
|
138
|
+
next unless resolved_path
|
|
139
|
+
next unless path_within_repo?(resolved_path)
|
|
140
|
+
|
|
141
|
+
file = if is_root
|
|
142
|
+
fetch_file_if_present(resolved_path)
|
|
143
|
+
else
|
|
144
|
+
fetch_file_from_host(resolved_path, fetch_submodules: true)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
next unless file
|
|
148
|
+
|
|
149
|
+
file.support_file = true
|
|
150
|
+
file
|
|
151
|
+
rescue Dependabot::DependencyFileNotFound
|
|
152
|
+
Dependabot.logger.info("License file not found: #{resolved_path}")
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
|
158
|
+
def extract_license_paths(pyproject_file)
|
|
159
|
+
parsed = TomlRB.parse(pyproject_file.content)
|
|
160
|
+
paths = []
|
|
161
|
+
|
|
162
|
+
# Handle legacy license = {file = "LICENSE"} format
|
|
163
|
+
license_decl = parsed.dig("project", "license")
|
|
164
|
+
paths << license_decl["file"] if license_decl.is_a?(Hash) && license_decl["file"].is_a?(String)
|
|
165
|
+
|
|
166
|
+
# Handle license-files = ["LICENSE", "LICENSES/*"] format (without glob expansion)
|
|
167
|
+
license_files_decl = parsed.dig("project", "license-files")
|
|
168
|
+
if license_files_decl.is_a?(Array)
|
|
169
|
+
license_files_decl.each do |pattern|
|
|
170
|
+
# Only include simple file paths, not glob patterns
|
|
171
|
+
paths << pattern if pattern.is_a?(String) && !pattern.include?("*")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
paths
|
|
176
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
177
|
+
[]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
sig { params(file_path: String, base_path: String).returns(T.nilable(String)) }
|
|
181
|
+
def resolve_support_file_path(file_path, base_path)
|
|
182
|
+
return nil if file_path.empty?
|
|
183
|
+
return nil if Pathname.new(file_path).absolute?
|
|
184
|
+
|
|
185
|
+
if base_path == directory || base_path == "."
|
|
186
|
+
clean_path(file_path)
|
|
187
|
+
else
|
|
188
|
+
clean_path(File.join(base_path, file_path))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
193
|
+
def version_source_files
|
|
194
|
+
return [] unless pyproject
|
|
195
|
+
|
|
196
|
+
files = []
|
|
197
|
+
files += fetch_version_source_files_for(directory, T.must(pyproject))
|
|
198
|
+
files += workspace_fetcher.version_source_files
|
|
199
|
+
|
|
200
|
+
files
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
sig do
|
|
204
|
+
params(
|
|
205
|
+
base_path: String,
|
|
206
|
+
pyproject_file: Dependabot::DependencyFile
|
|
207
|
+
).returns(T::Array[Dependabot::DependencyFile])
|
|
208
|
+
end
|
|
209
|
+
def fetch_version_source_files_for(base_path, pyproject_file)
|
|
210
|
+
version_paths = extract_version_source_paths(pyproject_file)
|
|
211
|
+
is_root = base_path == directory
|
|
212
|
+
|
|
213
|
+
version_paths.filter_map do |version_path|
|
|
214
|
+
resolved_path = resolve_support_file_path(version_path, base_path)
|
|
215
|
+
next unless resolved_path
|
|
216
|
+
next unless path_within_repo?(resolved_path)
|
|
217
|
+
|
|
218
|
+
file = if is_root
|
|
219
|
+
fetch_file_if_present(resolved_path)
|
|
220
|
+
else
|
|
221
|
+
fetch_file_from_host(resolved_path, fetch_submodules: true)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
next unless file
|
|
225
|
+
|
|
226
|
+
file.support_file = true
|
|
227
|
+
file
|
|
228
|
+
rescue Dependabot::DependencyFileNotFound
|
|
229
|
+
Dependabot.logger.info("Version source file not found: #{resolved_path}")
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
|
|
235
|
+
def extract_version_source_paths(pyproject_file)
|
|
236
|
+
parsed = TomlRB.parse(pyproject_file.content)
|
|
237
|
+
paths = []
|
|
238
|
+
|
|
239
|
+
hatch_version_path = parsed.dig("tool", "hatch", "version", "path")
|
|
240
|
+
paths << hatch_version_path if hatch_version_path.is_a?(String)
|
|
241
|
+
|
|
242
|
+
paths
|
|
243
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
244
|
+
[]
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
248
|
+
def path_within_repo?(path)
|
|
249
|
+
cleaned = clean_path(path)
|
|
250
|
+
!cleaned.start_with?("../", "/")
|
|
251
|
+
end
|
|
252
|
+
|
|
114
253
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
115
254
|
def uv_lock_files
|
|
116
255
|
req_txt_and_in_files.select { |f| f.name.end_with?("uv.lock") } +
|
|
@@ -22,6 +22,7 @@ module Dependabot
|
|
|
22
22
|
extend T::Sig
|
|
23
23
|
|
|
24
24
|
require_relative "pyproject_preparer"
|
|
25
|
+
require_relative "version_config_parser"
|
|
25
26
|
|
|
26
27
|
REQUIRED_FILES = %w(pyproject.toml uv.lock).freeze # At least one of these files should be present
|
|
27
28
|
|
|
@@ -41,19 +42,24 @@ module Dependabot
|
|
|
41
42
|
sig { returns(T.nilable(T::Array[T.nilable(String)])) }
|
|
42
43
|
attr_reader :index_urls
|
|
43
44
|
|
|
45
|
+
sig { returns(T.nilable(String)) }
|
|
46
|
+
attr_reader :repo_contents_path
|
|
47
|
+
|
|
44
48
|
sig do
|
|
45
49
|
params(
|
|
46
50
|
dependencies: T::Array[Dependency],
|
|
47
51
|
dependency_files: T::Array[DependencyFile],
|
|
48
52
|
credentials: T::Array[Dependabot::Credential],
|
|
49
|
-
index_urls: T.nilable(T::Array[T.nilable(String)])
|
|
53
|
+
index_urls: T.nilable(T::Array[T.nilable(String)]),
|
|
54
|
+
repo_contents_path: T.nilable(String)
|
|
50
55
|
).void
|
|
51
56
|
end
|
|
52
|
-
def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
|
|
57
|
+
def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil, repo_contents_path: nil)
|
|
53
58
|
@dependencies = dependencies
|
|
54
59
|
@dependency_files = dependency_files
|
|
55
60
|
@credentials = credentials
|
|
56
61
|
@index_urls = index_urls
|
|
62
|
+
@repo_contents_path = repo_contents_path
|
|
57
63
|
@prepared_pyproject = T.let(nil, T.nilable(String))
|
|
58
64
|
@updated_lockfile_content = T.let(nil, T.nilable(String))
|
|
59
65
|
@pyproject = T.let(nil, T.nilable(Dependabot::DependencyFile))
|
|
@@ -243,7 +249,7 @@ module Dependabot
|
|
|
243
249
|
|
|
244
250
|
sig { params(pyproject_content: String).returns(String) }
|
|
245
251
|
def updated_lockfile_content_for(pyproject_content)
|
|
246
|
-
SharedHelpers.
|
|
252
|
+
SharedHelpers.in_a_temporary_repo_directory(directory, repo_contents_path) do
|
|
247
253
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
248
254
|
write_temporary_dependency_files(pyproject_content)
|
|
249
255
|
|
|
@@ -267,36 +273,52 @@ module Dependabot
|
|
|
267
273
|
end
|
|
268
274
|
def handle_uv_error(error)
|
|
269
275
|
error_message = error.message
|
|
270
|
-
error_message_patterns = ["No solution found when resolving dependencies", "Failed to build"]
|
|
271
|
-
|
|
272
|
-
if error_message_patterns.any? { |value| error_message.include?(value) }
|
|
273
|
-
match_unresolvable_regex = error_message.scan(UV_UNRESOLVABLE_REGEX).last
|
|
274
|
-
match_failed_to_build_regex = error_message.scan(UV_BUILD_FAILED_REGEX).last
|
|
275
|
-
|
|
276
|
-
if match_unresolvable_regex
|
|
277
|
-
formatted_error = if match_unresolvable_regex.is_a?(Array)
|
|
278
|
-
match_unresolvable_regex.join
|
|
279
|
-
else
|
|
280
|
-
match_unresolvable_regex
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
277
|
+
if resolution_error?(error_message)
|
|
278
|
+
handle_resolution_error(error_message)
|
|
279
|
+
elsif error_message.include?(RESOLUTION_IMPOSSIBLE_ERROR)
|
|
280
|
+
raise Dependabot::DependencyFileNotResolvable, error_message
|
|
281
|
+
else
|
|
282
|
+
raise error
|
|
283
|
+
end
|
|
284
|
+
end
|
|
291
285
|
|
|
292
|
-
|
|
286
|
+
sig { params(error_message: String).returns(T::Boolean) }
|
|
287
|
+
def resolution_error?(error_message)
|
|
288
|
+
["No solution found when resolving dependencies", "Failed to build"].any? do |pattern|
|
|
289
|
+
error_message.include?(pattern)
|
|
293
290
|
end
|
|
291
|
+
end
|
|
294
292
|
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
sig { params(error_message: String).returns(T.noreturn) }
|
|
294
|
+
def handle_resolution_error(error_message)
|
|
295
|
+
match_unresolvable = error_message.scan(UV_UNRESOLVABLE_REGEX).last
|
|
296
|
+
match_build_failed = error_message.scan(UV_BUILD_FAILED_REGEX).last
|
|
297
|
+
|
|
298
|
+
if match_unresolvable
|
|
299
|
+
formatted_error = Array(match_unresolvable).join
|
|
300
|
+
conflicting_deps = extract_conflicting_dependencies(formatted_error)
|
|
301
|
+
raise Dependabot::UpdateNotPossible, conflicting_deps if conflicting_deps.any?
|
|
302
|
+
|
|
303
|
+
raise Dependabot::DependencyFileNotResolvable, formatted_error
|
|
297
304
|
end
|
|
298
305
|
|
|
299
|
-
|
|
306
|
+
formatted_error = match_build_failed ? Array(match_build_failed).join : error_message
|
|
307
|
+
raise Dependabot::DependencyFileNotResolvable, formatted_error
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
sig { params(error_message: String).returns(T::Array[String]) }
|
|
311
|
+
def extract_conflicting_dependencies(error_message)
|
|
312
|
+
# Extract conflicting dependency names from the error message
|
|
313
|
+
# Pattern: "Because <pkg>==<ver> depends on <dep>>=<ver> and your project depends on <dep>==<ver>"
|
|
314
|
+
normalized_message = error_message.gsub(/\s+/, " ")
|
|
315
|
+
conflict_pattern = /Because (\S+)==\S+ depends on (\S+)[><=!]+\S+ and your project depends on \2==\S+/
|
|
316
|
+
|
|
317
|
+
match = normalized_message.match(conflict_pattern)
|
|
318
|
+
return [] unless match
|
|
319
|
+
|
|
320
|
+
# Return both the package being updated and the blocking dependency
|
|
321
|
+
[match[1], match[2]].compact
|
|
300
322
|
end
|
|
301
323
|
|
|
302
324
|
sig { returns(T.nilable(String)) }
|
|
@@ -309,12 +331,17 @@ module Dependabot
|
|
|
309
331
|
# to the absolute latest version (which may be blocked by ignore rules)
|
|
310
332
|
dep_name = T.must(dependency).name
|
|
311
333
|
dep_version = T.must(dependency).version
|
|
312
|
-
|
|
334
|
+
# Strip extras from the package name for the uv lock command
|
|
335
|
+
# uv lock --upgrade-package expects the base package name without extras
|
|
336
|
+
base_dep_name = normalise(dep_name)
|
|
337
|
+
package_spec = dep_version ? "#{base_dep_name}==#{dep_version}" : base_dep_name
|
|
313
338
|
|
|
314
339
|
command = "pyenv exec uv lock --upgrade-package #{package_spec} #{options}"
|
|
315
340
|
fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
|
|
316
341
|
|
|
317
|
-
|
|
342
|
+
env_vars = explicit_index_env_vars.merge(setuptools_scm_pretend_version_env_vars)
|
|
343
|
+
|
|
344
|
+
run_command(command, fingerprint: fingerprint, env: env_vars)
|
|
318
345
|
end
|
|
319
346
|
|
|
320
347
|
sig { params(command: String, fingerprint: T.nilable(String), env: T::Hash[String, String]).returns(String) }
|
|
@@ -323,7 +350,7 @@ module Dependabot
|
|
|
323
350
|
SharedHelpers.run_shell_command(command, fingerprint: fingerprint, env: env)
|
|
324
351
|
end
|
|
325
352
|
|
|
326
|
-
sig { params(pyproject_content: String).
|
|
353
|
+
sig { params(pyproject_content: String).void }
|
|
327
354
|
def write_temporary_dependency_files(pyproject_content)
|
|
328
355
|
dependency_files.each do |file|
|
|
329
356
|
path = file.name
|
|
@@ -333,23 +360,34 @@ module Dependabot
|
|
|
333
360
|
|
|
334
361
|
# Overwrite the pyproject with updated content
|
|
335
362
|
File.write("pyproject.toml", pyproject_content)
|
|
363
|
+
|
|
364
|
+
ensure_version_file_directories
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
sig { void }
|
|
368
|
+
def ensure_version_file_directories
|
|
369
|
+
all_version_configs.each do |config|
|
|
370
|
+
config.write_paths.each do |write_path|
|
|
371
|
+
dir = Pathname.new(write_path).dirname
|
|
372
|
+
next if dir.to_s == "." || dir.to_s.empty?
|
|
373
|
+
|
|
374
|
+
Dependabot.logger.info("Creating directory for version file: #{dir}")
|
|
375
|
+
FileUtils.mkdir_p(dir)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
336
378
|
end
|
|
337
379
|
|
|
338
380
|
sig { void }
|
|
339
381
|
def setup_python_environment
|
|
340
|
-
# Use LanguageVersionManager to determine and install the appropriate Python version
|
|
341
382
|
Dependabot.logger.info("Setting up Python environment using LanguageVersionManager")
|
|
342
383
|
|
|
343
384
|
begin
|
|
344
|
-
# Install the required Python version
|
|
345
385
|
language_version_manager.install_required_python
|
|
346
386
|
|
|
347
|
-
# Set the local Python version
|
|
348
387
|
python_version = language_version_manager.python_version
|
|
349
388
|
Dependabot.logger.info("Setting Python version to #{python_version}")
|
|
350
389
|
SharedHelpers.run_shell_command("pyenv local #{python_version}")
|
|
351
390
|
|
|
352
|
-
# We don't need to install uv as it should be available in the Docker environment
|
|
353
391
|
Dependabot.logger.info("Using pre-installed uv package")
|
|
354
392
|
rescue StandardError => e
|
|
355
393
|
Dependabot.logger.warn("Error setting up Python environment: #{e.message}")
|
|
@@ -541,6 +579,11 @@ module Dependabot
|
|
|
541
579
|
)
|
|
542
580
|
end
|
|
543
581
|
|
|
582
|
+
sig { returns(String) }
|
|
583
|
+
def directory
|
|
584
|
+
dependency_files.first&.directory || "/"
|
|
585
|
+
end
|
|
586
|
+
|
|
544
587
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
545
588
|
def lockfile
|
|
546
589
|
@lockfile ||= T.let(uv_lock, T.nilable(Dependabot::DependencyFile))
|
|
@@ -562,6 +605,61 @@ module Dependabot
|
|
|
562
605
|
|
|
563
606
|
T.must(dependency).requirements.select { _1[:file].end_with?(*REQUIRED_FILES) }.any?
|
|
564
607
|
end
|
|
608
|
+
|
|
609
|
+
sig { returns(T::Hash[String, String]) }
|
|
610
|
+
def setuptools_scm_pretend_version_env_vars
|
|
611
|
+
env_vars = T.let({}, T::Hash[String, String])
|
|
612
|
+
|
|
613
|
+
all_version_configs.each do |config|
|
|
614
|
+
package_name = config.package_name
|
|
615
|
+
next if package_name.nil? || package_name.empty?
|
|
616
|
+
next unless config.dynamic_version?
|
|
617
|
+
|
|
618
|
+
package_env_name = package_name.upcase.gsub(/[-.]/, "_")
|
|
619
|
+
version = config.fallback_version || "0.0.0"
|
|
620
|
+
|
|
621
|
+
env_vars["SETUPTOOLS_SCM_PRETEND_VERSION_FOR_#{package_env_name}"] = version
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
env_vars
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
sig { returns(T::Array[VersionConfigParser::VersionConfig]) }
|
|
628
|
+
def all_version_configs
|
|
629
|
+
@all_version_configs ||= T.let(
|
|
630
|
+
begin
|
|
631
|
+
configs = []
|
|
632
|
+
|
|
633
|
+
root_content = pyproject&.content
|
|
634
|
+
if root_content
|
|
635
|
+
parser = VersionConfigParser.new(
|
|
636
|
+
pyproject_content: root_content,
|
|
637
|
+
base_path: ".",
|
|
638
|
+
repo_root: "."
|
|
639
|
+
)
|
|
640
|
+
configs << parser.parse
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
dependency_files
|
|
644
|
+
.select { |f| f.name.end_with?("pyproject.toml") && f.name != "pyproject.toml" }
|
|
645
|
+
.each do |member_pyproject|
|
|
646
|
+
member_content = member_pyproject.content
|
|
647
|
+
next unless member_content
|
|
648
|
+
|
|
649
|
+
base_path = Pathname.new(member_pyproject.name).dirname.to_s
|
|
650
|
+
parser = VersionConfigParser.new(
|
|
651
|
+
pyproject_content: member_content,
|
|
652
|
+
base_path: base_path,
|
|
653
|
+
repo_root: "."
|
|
654
|
+
)
|
|
655
|
+
configs << parser.parse
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
configs
|
|
659
|
+
end,
|
|
660
|
+
T.nilable(T::Array[VersionConfigParser::VersionConfig])
|
|
661
|
+
)
|
|
662
|
+
end
|
|
565
663
|
end
|
|
566
664
|
# rubocop:enable Metrics/ClassLength
|
|
567
665
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
require "dependabot/uv/file_updater"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Uv
|
|
12
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
13
|
+
class VersionConfigParser
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
class VersionConfig < T::Struct
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
prop :write_paths, T::Array[String], default: []
|
|
20
|
+
prop :source_paths, T::Array[String], default: []
|
|
21
|
+
prop :fallback_version, T.nilable(String), default: nil
|
|
22
|
+
prop :package_name, T.nilable(String), default: nil
|
|
23
|
+
|
|
24
|
+
sig { returns(T::Boolean) }
|
|
25
|
+
def dynamic_version?
|
|
26
|
+
write_paths.any? || source_paths.any?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(pyproject_content: String, base_path: String, repo_root: String).void }
|
|
31
|
+
def initialize(pyproject_content:, base_path: ".", repo_root: ".")
|
|
32
|
+
@pyproject_content = pyproject_content
|
|
33
|
+
@base_path = base_path
|
|
34
|
+
@repo_root = repo_root
|
|
35
|
+
@parsed_pyproject = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { returns(VersionConfig) }
|
|
39
|
+
def parse
|
|
40
|
+
VersionConfig.new(
|
|
41
|
+
write_paths: collect_write_paths,
|
|
42
|
+
source_paths: collect_source_paths,
|
|
43
|
+
fallback_version: extract_fallback_version,
|
|
44
|
+
package_name: extract_package_name
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
sig { returns(String) }
|
|
51
|
+
attr_reader :pyproject_content
|
|
52
|
+
|
|
53
|
+
sig { returns(String) }
|
|
54
|
+
attr_reader :base_path
|
|
55
|
+
|
|
56
|
+
sig { returns(String) }
|
|
57
|
+
attr_reader :repo_root
|
|
58
|
+
|
|
59
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
60
|
+
def parsed_pyproject
|
|
61
|
+
return @parsed_pyproject unless @parsed_pyproject.nil?
|
|
62
|
+
|
|
63
|
+
@parsed_pyproject = TomlRB.parse(pyproject_content)
|
|
64
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
65
|
+
@parsed_pyproject = {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { returns(T::Array[String]) }
|
|
69
|
+
def collect_write_paths
|
|
70
|
+
paths = []
|
|
71
|
+
paths += setuptools_scm_write_paths
|
|
72
|
+
paths += hatch_vcs_build_hook_write_paths
|
|
73
|
+
paths.compact.uniq
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(T::Array[String]) }
|
|
77
|
+
def collect_source_paths
|
|
78
|
+
paths = []
|
|
79
|
+
paths += hatch_version_source_paths
|
|
80
|
+
paths.compact.uniq
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { returns(T::Array[String]) }
|
|
84
|
+
def setuptools_scm_write_paths
|
|
85
|
+
scm_config = parsed_pyproject.dig("tool", "setuptools_scm")
|
|
86
|
+
return [] unless scm_config.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
paths = []
|
|
89
|
+
|
|
90
|
+
version_file = scm_config["version_file"]
|
|
91
|
+
paths << validate_and_resolve_path(version_file) if version_file.is_a?(String)
|
|
92
|
+
|
|
93
|
+
write_to = scm_config["write_to"]
|
|
94
|
+
paths << validate_and_resolve_path(write_to) if write_to.is_a?(String)
|
|
95
|
+
|
|
96
|
+
paths.compact
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { returns(T::Array[String]) }
|
|
100
|
+
def hatch_vcs_build_hook_write_paths
|
|
101
|
+
vcs_hook = parsed_pyproject.dig("tool", "hatch", "build", "hooks", "vcs")
|
|
102
|
+
return [] unless vcs_hook.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
paths = []
|
|
105
|
+
|
|
106
|
+
version_file = vcs_hook["version-file"]
|
|
107
|
+
paths << validate_and_resolve_path(version_file) if version_file.is_a?(String)
|
|
108
|
+
|
|
109
|
+
paths.compact
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
sig { returns(T::Array[String]) }
|
|
113
|
+
def hatch_version_source_paths
|
|
114
|
+
hatch_version = parsed_pyproject.dig("tool", "hatch", "version")
|
|
115
|
+
return [] unless hatch_version.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
paths = []
|
|
118
|
+
|
|
119
|
+
version_path = hatch_version["path"]
|
|
120
|
+
paths << validate_and_resolve_path(version_path) if version_path.is_a?(String)
|
|
121
|
+
|
|
122
|
+
paths.compact
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sig { returns(T.nilable(String)) }
|
|
126
|
+
def extract_fallback_version
|
|
127
|
+
scm_config = parsed_pyproject.dig("tool", "setuptools_scm")
|
|
128
|
+
if scm_config.is_a?(Hash)
|
|
129
|
+
fallback = scm_config["fallback_version"]
|
|
130
|
+
return fallback if fallback.is_a?(String)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
raw_options = parsed_pyproject.dig("tool", "hatch", "version", "raw-options")
|
|
134
|
+
if raw_options.is_a?(Hash)
|
|
135
|
+
fallback = raw_options["fallback_version"]
|
|
136
|
+
return fallback if fallback.is_a?(String)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { returns(T.nilable(String)) }
|
|
143
|
+
def extract_package_name
|
|
144
|
+
name = parsed_pyproject.dig("project", "name")
|
|
145
|
+
return name if name.is_a?(String)
|
|
146
|
+
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sig { params(path: String).returns(T.nilable(String)) }
|
|
151
|
+
def validate_and_resolve_path(path)
|
|
152
|
+
return nil if path.empty?
|
|
153
|
+
return nil if Pathname.new(path).absolute?
|
|
154
|
+
|
|
155
|
+
resolved = File.expand_path(path, base_path)
|
|
156
|
+
|
|
157
|
+
repo_root_expanded = File.expand_path(repo_root)
|
|
158
|
+
resolved_expanded = File.expand_path(resolved)
|
|
159
|
+
|
|
160
|
+
unless resolved_expanded.start_with?(repo_root_expanded)
|
|
161
|
+
Dependabot.logger.warn(
|
|
162
|
+
"Version config path '#{path}' resolves outside repository root, ignoring"
|
|
163
|
+
)
|
|
164
|
+
return nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
Pathname.new(resolved_expanded).relative_path_from(Pathname.new(repo_root_expanded)).to_s
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -64,7 +64,8 @@ module Dependabot
|
|
|
64
64
|
dependencies: dependencies,
|
|
65
65
|
dependency_files: dependency_files,
|
|
66
66
|
credentials: credentials,
|
|
67
|
-
index_urls: pip_compile_index_urls
|
|
67
|
+
index_urls: pip_compile_index_urls,
|
|
68
|
+
repo_contents_path: repo_contents_path
|
|
68
69
|
).updated_dependency_files
|
|
69
70
|
end
|
|
70
71
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-uv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.358.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.358.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.358.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: dependabot-python
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.358.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.358.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: debug
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -274,6 +274,7 @@ files:
|
|
|
274
274
|
- lib/dependabot/uv/file_updater/pyproject_preparer.rb
|
|
275
275
|
- lib/dependabot/uv/file_updater/requirement_file_updater.rb
|
|
276
276
|
- lib/dependabot/uv/file_updater/requirement_replacer.rb
|
|
277
|
+
- lib/dependabot/uv/file_updater/version_config_parser.rb
|
|
277
278
|
- lib/dependabot/uv/language.rb
|
|
278
279
|
- lib/dependabot/uv/language_version_manager.rb
|
|
279
280
|
- lib/dependabot/uv/metadata_finder.rb
|
|
@@ -297,7 +298,7 @@ licenses:
|
|
|
297
298
|
- MIT
|
|
298
299
|
metadata:
|
|
299
300
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
300
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
301
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.358.0
|
|
301
302
|
rdoc_options: []
|
|
302
303
|
require_paths:
|
|
303
304
|
- lib
|
|
@@ -312,7 +313,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
312
313
|
- !ruby/object:Gem::Version
|
|
313
314
|
version: 3.3.0
|
|
314
315
|
requirements: []
|
|
315
|
-
rubygems_version: 3.
|
|
316
|
+
rubygems_version: 3.7.2
|
|
316
317
|
specification_version: 4
|
|
317
318
|
summary: Provides Dependabot support for Python uv
|
|
318
319
|
test_files: []
|