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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ab87b3858f48fd897503d6999fd5a92c7f1b79902a23991acac4f8404e770e4
4
- data.tar.gz: 32ccacada81451af81b63c5d9e2fc3bc9782444e40c542b2e3fe6b4bbf672c64
3
+ metadata.gz: b61cf200acbd26b1c5b680d31b17cee5ef1fa9a80b79d7be51907f325fe32b75
4
+ data.tar.gz: 5cd07c0084c8d303ffa2354338b378ae0fd180d89a0970b52b37d66d9b8775c8
5
5
  SHA512:
6
- metadata.gz: 72e6314b00aeddfc38f3cff0caf769cff9d4b1b02b3cd222ff77ce4ee884917ae5ec4ad1a72e86446eb1258ee11e1ee3140454b4ba7bf4fccc21aaf86c6501a2
7
- data.tar.gz: b823d324780b32908ce41c0eb02d937bf45d9070e5c211faca0cff7e7b1123a01d7fc2bd6f8154d5dcb7b821897acbdf9c8c88a6f0219011378c9882306b8dc5
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.in_a_temporary_directory do
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
- if match_failed_to_build_regex
285
- formatted_error = if match_failed_to_build_regex.is_a?(Array)
286
- match_failed_to_build_regex.join
287
- else
288
- match_failed_to_build_regex
289
- end
290
- end
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
- raise Dependabot::DependencyFileNotResolvable, formatted_error
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
- if error_message.include?(RESOLUTION_IMPOSSIBLE_ERROR)
296
- raise Dependabot::DependencyFileNotResolvable, error_message
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
- raise error
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
- run_command(command, fingerprint: fingerprint, env: explicit_index_env_vars)
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).returns(Integer) }
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.357.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.357.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.357.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.357.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.357.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.357.0
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