dependabot-uv 0.357.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 +128 -33
- data/lib/dependabot/uv/file_updater/version_config_parser.rb +172 -0
- data/lib/dependabot/uv/file_updater.rb +2 -1
- 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: 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)) }
|
|
@@ -317,7 +339,9 @@ module Dependabot
|
|
|
317
339
|
command = "pyenv exec uv lock --upgrade-package #{package_spec} #{options}"
|
|
318
340
|
fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
|
|
319
341
|
|
|
320
|
-
|
|
342
|
+
env_vars = explicit_index_env_vars.merge(setuptools_scm_pretend_version_env_vars)
|
|
343
|
+
|
|
344
|
+
run_command(command, fingerprint: fingerprint, env: env_vars)
|
|
321
345
|
end
|
|
322
346
|
|
|
323
347
|
sig { params(command: String, fingerprint: T.nilable(String), env: T::Hash[String, String]).returns(String) }
|
|
@@ -326,7 +350,7 @@ module Dependabot
|
|
|
326
350
|
SharedHelpers.run_shell_command(command, fingerprint: fingerprint, env: env)
|
|
327
351
|
end
|
|
328
352
|
|
|
329
|
-
sig { params(pyproject_content: String).
|
|
353
|
+
sig { params(pyproject_content: String).void }
|
|
330
354
|
def write_temporary_dependency_files(pyproject_content)
|
|
331
355
|
dependency_files.each do |file|
|
|
332
356
|
path = file.name
|
|
@@ -336,23 +360,34 @@ module Dependabot
|
|
|
336
360
|
|
|
337
361
|
# Overwrite the pyproject with updated content
|
|
338
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
|
|
339
378
|
end
|
|
340
379
|
|
|
341
380
|
sig { void }
|
|
342
381
|
def setup_python_environment
|
|
343
|
-
# Use LanguageVersionManager to determine and install the appropriate Python version
|
|
344
382
|
Dependabot.logger.info("Setting up Python environment using LanguageVersionManager")
|
|
345
383
|
|
|
346
384
|
begin
|
|
347
|
-
# Install the required Python version
|
|
348
385
|
language_version_manager.install_required_python
|
|
349
386
|
|
|
350
|
-
# Set the local Python version
|
|
351
387
|
python_version = language_version_manager.python_version
|
|
352
388
|
Dependabot.logger.info("Setting Python version to #{python_version}")
|
|
353
389
|
SharedHelpers.run_shell_command("pyenv local #{python_version}")
|
|
354
390
|
|
|
355
|
-
# We don't need to install uv as it should be available in the Docker environment
|
|
356
391
|
Dependabot.logger.info("Using pre-installed uv package")
|
|
357
392
|
rescue StandardError => e
|
|
358
393
|
Dependabot.logger.warn("Error setting up Python environment: #{e.message}")
|
|
@@ -544,6 +579,11 @@ module Dependabot
|
|
|
544
579
|
)
|
|
545
580
|
end
|
|
546
581
|
|
|
582
|
+
sig { returns(String) }
|
|
583
|
+
def directory
|
|
584
|
+
dependency_files.first&.directory || "/"
|
|
585
|
+
end
|
|
586
|
+
|
|
547
587
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
548
588
|
def lockfile
|
|
549
589
|
@lockfile ||= T.let(uv_lock, T.nilable(Dependabot::DependencyFile))
|
|
@@ -565,6 +605,61 @@ module Dependabot
|
|
|
565
605
|
|
|
566
606
|
T.must(dependency).requirements.select { _1[:file].end_with?(*REQUIRED_FILES) }.any?
|
|
567
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
|
|
568
663
|
end
|
|
569
664
|
# rubocop:enable Metrics/ClassLength
|
|
570
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
|