dependabot-uv 0.351.0 → 0.353.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: f74cec4813c7f6b11b51b6f73a10db68327ca41a2737e469ba5d9ea3b4612bea
4
- data.tar.gz: e17b701d94aeafdca486c2303d23ea3b423eebbddd93d347ce1224160920cab5
3
+ metadata.gz: c7a64a9814f7e13db97058e97650e562835fd427001c18b08d9d13ff426e0b9f
4
+ data.tar.gz: f19a7c8dbc8208cfdba09e5f5513b04895b6ff4eb32c5cba0690bb21e2a2db6d
5
5
  SHA512:
6
- metadata.gz: 32c427d81b22edb5d8b145ced9080a7fca9264e371da8f4a9ff5ca57c89b58f69179b4ade15620e24bb6a5d43194d959489d0c10942b050be8fb5e0c13327121
7
- data.tar.gz: 615a296656b8957f897e5b17e3f951b143579bbc08aa8e8c806928bcd3e3349e4f0e3a54f0dcf3b5449fa7c8b6a892677e9a865925ec1fb044094ee35f4de527
6
+ metadata.gz: ff7a174888fb664c4358198498c99eb6179b23c259297a8693b213609b876b8e8fd3350206e928f10bd51654c6f0a64987b28ad792dab1d72ab813d85574cae4
7
+ data.tar.gz: a94975fe714eae7eadac990e4ce9c8d996ac4478b0016557245fb5fc2e5745591832f2098d1ca0383bcb7de025cf0e85b047aeefed2baaa8b602e90ac4d0431f
@@ -32,7 +32,7 @@ def parse_pep621_pep735_dependencies(pyproject_path):
32
32
  next(iter(specifier_set)).operator in {"==", "==="}):
33
33
  return next(iter(specifier_set)).version
34
34
 
35
- def parse_requirement(entry, pyproject_path):
35
+ def parse_requirement(entry, pyproject_path, requirement_type=None):
36
36
  try:
37
37
  req = Requirement(entry)
38
38
  except InvalidRequirement as e:
@@ -46,14 +46,19 @@ def parse_pep621_pep735_dependencies(pyproject_path):
46
46
  "file": pyproject_path,
47
47
  "requirement": str(req.specifier),
48
48
  "extras": sorted(list(req.extras)),
49
+ "requirement_type": requirement_type,
49
50
  }
50
51
  return data
51
52
 
52
- def parse_toml_section_pep621_dependencies(pyproject_path, dependencies):
53
+ def parse_toml_section_pep621_dependencies(
54
+ pyproject_path, dependencies, requirement_type=None
55
+ ):
53
56
  requirement_packages = []
54
57
 
55
58
  for dependency in dependencies:
56
- parsed_dependency = parse_requirement(dependency, pyproject_path)
59
+ parsed_dependency = parse_requirement(
60
+ dependency, pyproject_path, requirement_type
61
+ )
57
62
  requirement_packages.append(parsed_dependency)
58
63
 
59
64
  return requirement_packages
@@ -75,7 +80,9 @@ def parse_pep621_pep735_dependencies(pyproject_path):
75
80
  for entry in dependencies:
76
81
  # Handle direct requirement
77
82
  if isinstance(entry, str):
78
- parsed_dependency = parse_requirement(entry, pyproject_path)
83
+ parsed_dependency = parse_requirement(
84
+ entry, pyproject_path, group_name
85
+ )
79
86
  requirement_packages.append(parsed_dependency)
80
87
  # Handle include-group directive
81
88
  elif isinstance(entry, dict) and "include-group" in entry:
@@ -128,7 +135,8 @@ def parse_pep621_pep735_dependencies(pyproject_path):
128
135
  if 'requires' in build_system_section:
129
136
  build_system_dependencies = parse_toml_section_pep621_dependencies(
130
137
  pyproject_path,
131
- build_system_section['requires']
138
+ build_system_section['requires'],
139
+ "build-system"
132
140
  )
133
141
  dependencies.extend(build_system_dependencies)
134
142
 
@@ -0,0 +1,210 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "toml-rb"
5
+ require "sorbet-runtime"
6
+ require "dependabot/dependency_file"
7
+
8
+ module Dependabot
9
+ module Uv
10
+ class FileFetcher < Dependabot::FileFetchers::Base
11
+ class WorkspaceFetcher
12
+ extend T::Sig
13
+
14
+ README_FILENAMES = T.let(%w(README.md README.rst README.txt README).freeze, T::Array[String])
15
+
16
+ sig do
17
+ params(
18
+ file_fetcher: Dependabot::Uv::FileFetcher,
19
+ pyproject: T.nilable(Dependabot::DependencyFile)
20
+ ).void
21
+ end
22
+ def initialize(file_fetcher, pyproject)
23
+ @file_fetcher = file_fetcher
24
+ @pyproject = pyproject
25
+ @parsed_pyproject = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
26
+ end
27
+
28
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
29
+ def workspace_member_files
30
+ return [] unless @pyproject
31
+
32
+ workspace_member_paths.flat_map do |member_path|
33
+ member_pyproject = fetch_workspace_member_pyproject(member_path)
34
+ member_readmes = fetch_readme_files_for(member_path, member_pyproject)
35
+
36
+ [member_pyproject] + member_readmes
37
+ rescue Dependabot::DependencyFileNotFound
38
+ []
39
+ end
40
+ end
41
+
42
+ sig { returns(T::Array[{ name: String, file: String }]) }
43
+ def uv_sources_workspace_dependencies
44
+ return [] unless @pyproject
45
+
46
+ uv_sources = parsed_pyproject.dig("tool", "uv", "sources")
47
+ return [] unless uv_sources
48
+
49
+ uv_sources.filter_map do |name, source_config|
50
+ if source_config.is_a?(Hash) && source_config["workspace"] == true
51
+ {
52
+ name: T.cast(name, String),
53
+ file: @pyproject.name
54
+ }
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ sig { params(member_path: String).returns(Dependabot::DependencyFile) }
62
+ def fetch_workspace_member_pyproject(member_path)
63
+ pyproject_path = clean_path(File.join(member_path, "pyproject.toml"))
64
+ pyproject_file = fetch_file_from_host(pyproject_path, fetch_submodules: true)
65
+ pyproject_file.support_file = true
66
+ pyproject_file
67
+ end
68
+
69
+ sig do
70
+ params(
71
+ path: String,
72
+ pyproject_file: Dependabot::DependencyFile
73
+ ).returns(T::Array[Dependabot::DependencyFile])
74
+ end
75
+ def fetch_readme_files_for(path, pyproject_file)
76
+ readme_candidates = readme_candidates_from_pyproject(pyproject_file)
77
+ is_root_project = path == directory
78
+
79
+ readme_candidates.filter_map do |filename|
80
+ file = fetch_readme_file(filename, path, is_root_project)
81
+ next unless file
82
+
83
+ file.support_file = true
84
+ file
85
+ rescue Dependabot::DependencyFileNotFound
86
+ nil
87
+ end
88
+ rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
89
+ []
90
+ end
91
+
92
+ sig { params(pyproject_file: Dependabot::DependencyFile).returns(T::Array[String]) }
93
+ def readme_candidates_from_pyproject(pyproject_file)
94
+ parsed_content = TomlRB.parse(pyproject_file.content)
95
+ readme_declaration = parsed_content.dig("project", "readme")
96
+
97
+ case readme_declaration
98
+ when String then [readme_declaration]
99
+ when Hash
100
+ readme_declaration["file"].is_a?(String) ? [T.cast(readme_declaration["file"], String)] : README_FILENAMES
101
+ else
102
+ README_FILENAMES
103
+ end
104
+ end
105
+
106
+ sig do
107
+ params(
108
+ filename: String,
109
+ path: String,
110
+ is_root_project: T::Boolean
111
+ ).returns(T.nilable(Dependabot::DependencyFile))
112
+ end
113
+ def fetch_readme_file(filename, path, is_root_project)
114
+ if is_root_project
115
+ fetch_file_if_present(filename)
116
+ else
117
+ file_path = clean_path(File.join(path, filename))
118
+ fetch_file_from_host(file_path, fetch_submodules: true)
119
+ end
120
+ end
121
+
122
+ sig { returns(T::Array[String]) }
123
+ def workspace_member_paths
124
+ return [] unless @pyproject
125
+
126
+ members = parsed_pyproject.dig("tool", "uv", "workspace", "members")
127
+ return [] unless members.is_a?(Array)
128
+
129
+ members.grep(String).flat_map { |pattern| expand_workspace_pattern(pattern) }
130
+ end
131
+
132
+ sig { params(pattern: String).returns(T::Array[String]) }
133
+ def expand_workspace_pattern(pattern)
134
+ return [pattern] unless pattern.include?("*")
135
+
136
+ base_directory = extract_base_directory_from_glob(pattern)
137
+ directory_paths = fetch_directory_paths_for_matching(base_directory)
138
+ match_paths_against_pattern(directory_paths, pattern)
139
+ end
140
+
141
+ sig { params(glob_pattern: String).returns(String) }
142
+ def extract_base_directory_from_glob(glob_pattern)
143
+ pattern_without_dot_slash = glob_pattern.gsub(%r{^\./}, "")
144
+ path_before_glob = pattern_without_dot_slash.split("*").first&.gsub(%r{(?<=/)[^/]*$}, "") || "."
145
+ path_before_glob.empty? ? "." : path_before_glob.chomp("/")
146
+ end
147
+
148
+ sig { params(base_dir: String).returns(T::Array[String]) }
149
+ def fetch_directory_paths_for_matching(base_dir)
150
+ normalized_directory = directory.gsub(%r{(^/|/$)}, "")
151
+
152
+ repo_contents(dir: base_dir, raise_errors: false)
153
+ .select { |file| T.unsafe(file).type == "dir" }
154
+ .map { |f| T.unsafe(f).path.gsub(%r{^/?#{Regexp.escape(normalized_directory)}/?}, "") }
155
+ end
156
+
157
+ sig { params(paths: T::Array[String], pattern: String).returns(T::Array[String]) }
158
+ def match_paths_against_pattern(paths, pattern)
159
+ pattern_without_dot_slash = pattern.gsub(%r{^\./}, "")
160
+ paths.select { |path| File.fnmatch?(pattern_without_dot_slash, path, File::FNM_PATHNAME) }
161
+ end
162
+
163
+ sig { returns(T::Hash[String, T.untyped]) }
164
+ def parsed_pyproject
165
+ cached = @parsed_pyproject
166
+ return cached if cached
167
+ return {} unless @pyproject
168
+
169
+ @parsed_pyproject = TomlRB.parse(@pyproject.content)
170
+ end
171
+
172
+ # Delegate methods to file_fetcher
173
+ sig { params(path: T.nilable(T.any(Pathname, String))).returns(String) }
174
+ def clean_path(path)
175
+ @file_fetcher.send(:cleanpath, path)
176
+ end
177
+
178
+ sig do
179
+ params(
180
+ filename: String,
181
+ fetch_submodules: T::Boolean
182
+ ).returns(Dependabot::DependencyFile)
183
+ end
184
+ def fetch_file_from_host(filename, fetch_submodules: false)
185
+ @file_fetcher.send(:fetch_file_from_host, filename, fetch_submodules: fetch_submodules)
186
+ end
187
+
188
+ sig { params(filename: String).returns(T.nilable(Dependabot::DependencyFile)) }
189
+ def fetch_file_if_present(filename)
190
+ @file_fetcher.send(:fetch_file_if_present, filename)
191
+ end
192
+
193
+ sig do
194
+ params(
195
+ dir: T.nilable(String),
196
+ raise_errors: T::Boolean
197
+ ).returns(T::Array[OpenStruct])
198
+ end
199
+ def repo_contents(dir: nil, raise_errors: true)
200
+ @file_fetcher.send(:repo_contents, dir: dir, raise_errors: raise_errors)
201
+ end
202
+
203
+ sig { returns(String) }
204
+ def directory
205
+ @file_fetcher.send(:directory)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -12,6 +12,7 @@ require "dependabot/uv/requirements_file_matcher"
12
12
  require "dependabot/uv/requirement_parser"
13
13
  require "dependabot/uv/file_parser/pyproject_files_parser"
14
14
  require "dependabot/uv/file_parser/python_requirement_parser"
15
+ require "dependabot/uv/file_fetcher/workspace_fetcher"
15
16
  require "dependabot/errors"
16
17
  require "dependabot/file_filtering"
17
18
 
@@ -94,6 +95,7 @@ module Dependabot
94
95
 
95
96
  fetched_files += uv_lock_files
96
97
  fetched_files += project_files
98
+ fetched_files += workspace_member_files
97
99
  fetched_files << python_version_file if python_version_file
98
100
 
99
101
  uniques = uniq_files(fetched_files)
@@ -119,38 +121,20 @@ module Dependabot
119
121
  end
120
122
 
121
123
  sig { returns(T::Array[Dependabot::DependencyFile]) }
122
- def readme_files
123
- return [] unless pyproject
124
+ def workspace_member_files
125
+ workspace_fetcher.workspace_member_files
126
+ end
124
127
 
125
- # Attempt to read the readme declaration from the pyproject. Accept both simplified
126
- # string form and table form ( { file = "..." } ).
127
- readme_decl = nil
128
- begin
129
- readme_decl = parsed_pyproject.dig("project", "readme")
130
- rescue TomlRB::ParseError
131
- # If the pyproject is unparseable fail later in parsed_pyproject.
132
- end
128
+ sig { returns(WorkspaceFetcher) }
129
+ def workspace_fetcher
130
+ @workspace_fetcher ||= T.let(WorkspaceFetcher.new(self, pyproject), T.nilable(WorkspaceFetcher))
131
+ end
133
132
 
134
- candidate_names =
135
- case readme_decl
136
- when String then [readme_decl]
137
- when Hash
138
- if readme_decl["file"].is_a?(String)
139
- [T.cast(readme_decl["file"], String)]
140
- else
141
- README_FILENAMES
142
- end
143
- else
144
- README_FILENAMES
145
- end
133
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
134
+ def readme_files
135
+ return [] unless pyproject
146
136
 
147
- candidate_names.filter_map do |filename|
148
- file = fetch_file_if_present(filename)
149
- file.support_file = true if file
150
- file
151
- rescue Dependabot::DependencyFileNotFound
152
- nil
153
- end
137
+ workspace_fetcher.send(:fetch_readme_files_for, directory, T.must(pyproject))
154
138
  end
155
139
 
156
140
  sig { returns(T::Array[Dependabot::DependencyFile]) }
@@ -472,6 +456,11 @@ module Dependabot
472
456
  end
473
457
  end
474
458
 
459
+ sig { returns(T::Array[{ name: String, file: String }]) }
460
+ def uv_sources_workspace_dependencies
461
+ workspace_fetcher.uv_sources_workspace_dependencies
462
+ end
463
+
475
464
  sig { params(path: T.nilable(T.any(Pathname, String))).returns(T::Array[Dependabot::DependencyFile]) }
476
465
  def fetch_requirement_files_from_path(path = nil)
477
466
  contents = path ? repo_contents(dir: path) : repo_contents
@@ -37,6 +37,7 @@ module Dependabot
37
37
  "pip._internal.exceptions.InstallationSubprocessError: Getting requirements to build wheel exited with 1",
38
38
  String
39
39
  )
40
+ PYTHON_VERSION_REGEX = T.let(/--python-version[=\s]+(?<version>\d+\.\d+(?:\.\d+)?)/, Regexp)
40
41
 
41
42
  sig { returns(T::Array[Dependabot::Dependency]) }
42
43
  attr_reader :dependencies
@@ -475,6 +476,8 @@ module Dependabot
475
476
  /--index-url=\S+/, "--index-url=<index_url>"
476
477
  ).sub(
477
478
  /--extra-index-url=\S+/, "--extra-index-url=<extra_index_url>"
479
+ ).sub(
480
+ /--python-version=\S+/, "--python-version=<python_version>"
478
481
  )
479
482
  end
480
483
 
@@ -492,21 +495,27 @@ module Dependabot
492
495
 
493
496
  sig { params(requirements_file: Dependabot::DependencyFile).returns(T::Array[String]) }
494
497
  def uv_compile_options_from_compiled_file(requirements_file)
498
+ content = T.must(requirements_file.content)
495
499
  options = ["--output-file=#{requirements_file.name}"]
496
- options << "--emit-index-url" if T.must(requirements_file.content).include?("index-url http")
497
- options << "--generate-hashes" if T.must(requirements_file.content).include?("--hash=sha")
498
- options << "--no-annotate" unless T.must(requirements_file.content).include?("# via ")
499
- options << "--pre" if T.must(requirements_file.content).include?("--pre")
500
- options << "--no-strip-extras" if T.must(requirements_file.content).include?("--no-strip-extras")
501
-
502
- if T.must(requirements_file.content).include?("--no-binary") ||
503
- T.must(requirements_file.content).include?("--only-binary")
504
- options << "--emit-build-options"
505
- end
500
+ options << "--emit-index-url" if content.include?("index-url http")
501
+ options << "--generate-hashes" if content.include?("--hash=sha")
502
+ options << "--no-annotate" unless content.include?("# via ")
503
+ options << "--pre" if content.include?("--pre")
504
+ options << "--no-strip-extras" if content.include?("--no-strip-extras")
505
+ options << "--emit-build-options" if content.include?("--no-binary") || content.include?("--only-binary")
506
+ options << "--universal" if content.include?("--universal")
507
+
508
+ python_version_option = extract_python_version_option(content)
509
+ options << python_version_option if python_version_option
510
+
511
+ options.compact
512
+ end
506
513
 
507
- options << "--universal" if T.must(requirements_file.content).include?("--universal")
514
+ sig { params(content: String).returns(T.nilable(String)) }
515
+ def extract_python_version_option(content)
516
+ return unless (match = PYTHON_VERSION_REGEX.match(content))
508
517
 
509
- options
518
+ "--python-version=#{match[:version]}"
510
519
  end
511
520
 
512
521
  sig { returns(T::Array[String]) }
@@ -12,10 +12,12 @@ require "dependabot/uv/file_parser/python_requirement_parser"
12
12
  require "dependabot/uv/file_updater"
13
13
  require "dependabot/uv/native_helpers"
14
14
  require "dependabot/uv/name_normaliser"
15
+ require "dependabot/uv/requirement_suffix_helper"
15
16
 
16
17
  module Dependabot
17
18
  module Uv
18
19
  class FileUpdater
20
+ # rubocop:disable Metrics/ClassLength
19
21
  class LockFileUpdater
20
22
  extend T::Sig
21
23
 
@@ -73,6 +75,16 @@ module Dependabot
73
75
  T.must(dependencies.first)
74
76
  end
75
77
 
78
+ sig { returns(T::Boolean) }
79
+ def build_system_only_dependency?
80
+ return false unless dependency
81
+
82
+ groups = T.must(dependency).requirements.flat_map { |req| req[:groups] || [] }.compact.uniq
83
+ return false if groups.empty?
84
+
85
+ groups.all?("build-system")
86
+ end
87
+
76
88
  sig { returns(T::Array[Dependabot::DependencyFile]) }
77
89
  def fetch_updated_dependency_files
78
90
  return [] unless create_or_update_lock_file?
@@ -87,9 +99,10 @@ module Dependabot
87
99
  )
88
100
  end
89
101
 
90
- if lockfile
102
+ if lockfile && !build_system_only_dependency?
91
103
  # Use updated_lockfile_content which might raise if the lockfile doesn't change
92
104
  new_content = updated_lockfile_content
105
+
93
106
  raise "Expected lockfile to change!" if T.must(lockfile).content == new_content
94
107
 
95
108
  updated_files << updated_file(file: T.must(lockfile), content: new_content)
@@ -127,7 +140,7 @@ module Dependabot
127
140
  def replace_dep(dep, content, new_r, old_r)
128
141
  new_req = new_r[:requirement]
129
142
  old_req = old_r[:requirement]
130
- escaped_name = Regexp.escape(dep.name)
143
+ escaped_name = escape_package_name(dep.name)
131
144
 
132
145
  regex = /(["']#{escaped_name})([^"']+)(["'])/x
133
146
 
@@ -136,17 +149,23 @@ module Dependabot
136
149
  updated_content = content.gsub(regex) do
137
150
  captured_requirement = Regexp.last_match(2)
138
151
 
139
- if requirements_match?(T.must(captured_requirement), old_req)
152
+ requirement_body, suffix = RequirementSuffixHelper.split(T.must(captured_requirement))
153
+
154
+ next Regexp.last_match(0) unless old_req
155
+
156
+ if requirements_match?(T.must(requirement_body), old_req)
140
157
  replaced = true
141
- "#{Regexp.last_match(1)}#{new_req}#{Regexp.last_match(3)}"
158
+ "#{Regexp.last_match(1)}#{new_req}#{suffix}#{Regexp.last_match(3)}"
142
159
  else
143
160
  Regexp.last_match(0)
144
161
  end
145
162
  end
146
-
147
163
  unless replaced
148
164
  updated_content = content.sub(regex) do
149
- "#{Regexp.last_match(1)}#{new_req}#{Regexp.last_match(3)}"
165
+ captured_requirement = Regexp.last_match(2)
166
+ _, suffix = RequirementSuffixHelper.split(T.must(captured_requirement))
167
+
168
+ "#{Regexp.last_match(1)}#{new_req}#{suffix}#{Regexp.last_match(3)}"
150
169
  end
151
170
  end
152
171
 
@@ -155,11 +174,12 @@ module Dependabot
155
174
 
156
175
  sig { params(req1: String, req2: String).returns(T::Boolean) }
157
176
  def requirements_match?(req1, req2)
158
- normalize = lambda do |req|
159
- req.split(",").map(&:strip).sort.join(",")
160
- end
177
+ normalized_requirement(req1) == normalized_requirement(req2)
178
+ end
161
179
 
162
- normalize.call(req1) == normalize.call(req2)
180
+ sig { params(req: String).returns(String) }
181
+ def normalized_requirement(req)
182
+ req.split(",").map(&:strip).sort.join(",")
163
183
  end
164
184
 
165
185
  sig { returns(String) }
@@ -374,8 +394,9 @@ module Dependabot
374
394
  end
375
395
 
376
396
  sig { params(name: T.any(String, Symbol)).returns(String) }
377
- def escape(name)
378
- Regexp.escape(name).gsub("\\-", "[-_.]")
397
+ def escape_package_name(name)
398
+ # Per PEP 503, Python package names normalize -, _, and . to the same character
399
+ Regexp.escape(name).gsub(/\\[-_.]/, "[-_.]")
379
400
  end
380
401
 
381
402
  sig { params(file: T.nilable(DependencyFile)).returns(T::Boolean) }
@@ -456,9 +477,12 @@ module Dependabot
456
477
 
457
478
  sig { returns(T::Boolean) }
458
479
  def create_or_update_lock_file?
480
+ return true if lockfile && T.must(dependency).requirements.empty?
481
+
459
482
  T.must(dependency).requirements.select { _1[:file].end_with?(*REQUIRED_FILES) }.any?
460
483
  end
461
484
  end
485
+ # rubocop:enable Metrics/ClassLength
462
486
  end
463
487
  end
464
488
  end
@@ -0,0 +1,29 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Uv
8
+ module RequirementSuffixHelper
9
+ extend T::Sig
10
+
11
+ REQUIREMENT_SUFFIX_REGEX = T.let(
12
+ Regexp.new(
13
+ "\\A(?<requirement>.*?)(?<suffix>\\s*(?:;|#).*)?\\z",
14
+ Regexp::MULTILINE
15
+ ).freeze,
16
+ Regexp
17
+ )
18
+
19
+ sig { params(segment: String).returns(T::Array[String]) }
20
+ def self.split(segment)
21
+ match = REQUIREMENT_SUFFIX_REGEX.match(segment)
22
+ requirement = match ? match[:requirement] : segment
23
+ suffix = match&.[](:suffix) || ""
24
+
25
+ [T.must(requirement).strip, suffix]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -6,6 +6,7 @@ require "sorbet-runtime"
6
6
  require "dependabot/uv/version"
7
7
  require "dependabot/uv/requirement"
8
8
  require "dependabot/uv/update_checker"
9
+ require "dependabot/uv/update_checker/latest_version_finder"
9
10
 
10
11
  module Dependabot
11
12
  module Uv
@@ -18,14 +19,25 @@ module Dependabot
18
19
  dependency: Dependabot::Dependency,
19
20
  dependency_files: T::Array[Dependabot::DependencyFile],
20
21
  credentials: T::Array[Dependabot::Credential],
21
- repo_contents_path: T.nilable(String)
22
+ repo_contents_path: T.nilable(String),
23
+ security_advisories: T::Array[Dependabot::SecurityAdvisory],
24
+ ignored_versions: T::Array[String]
22
25
  ).void
23
26
  end
24
- def initialize(dependency:, dependency_files:, credentials:, repo_contents_path: nil)
27
+ def initialize(
28
+ dependency:,
29
+ dependency_files:,
30
+ credentials:,
31
+ repo_contents_path: nil,
32
+ security_advisories: [],
33
+ ignored_versions: []
34
+ )
25
35
  @dependency = dependency
26
36
  @dependency_files = dependency_files
27
37
  @credentials = credentials
28
38
  @repo_contents_path = repo_contents_path
39
+ @security_advisories = security_advisories
40
+ @ignored_versions = ignored_versions
29
41
  end
30
42
 
31
43
  sig { params(requirement: T.nilable(String)).returns(T.nilable(Dependabot::Uv::Version)) }
@@ -50,7 +62,12 @@ module Dependabot
50
62
 
51
63
  sig { returns(T.nilable(Dependabot::Uv::Version)) }
52
64
  def lowest_resolvable_security_fix_version
53
- nil
65
+ # Delegate to LatestVersionFinder which handles security advisory filtering
66
+ fix_version = latest_version_finder.lowest_security_fix_version
67
+ return nil if fix_version.nil?
68
+
69
+ # Return the fix version cast to Uv::Version
70
+ Uv::Version.new(fix_version.to_s)
54
71
  end
55
72
 
56
73
  private
@@ -66,6 +83,27 @@ module Dependabot
66
83
 
67
84
  sig { returns(T.nilable(String)) }
68
85
  attr_reader :repo_contents_path
86
+
87
+ sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
88
+ attr_reader :security_advisories
89
+
90
+ sig { returns(T::Array[String]) }
91
+ attr_reader :ignored_versions
92
+
93
+ sig { returns(LatestVersionFinder) }
94
+ def latest_version_finder
95
+ @latest_version_finder ||= T.let(
96
+ LatestVersionFinder.new(
97
+ dependency: dependency,
98
+ dependency_files: dependency_files,
99
+ credentials: credentials,
100
+ ignored_versions: ignored_versions,
101
+ security_advisories: security_advisories,
102
+ raise_on_ignored: false
103
+ ),
104
+ T.nilable(LatestVersionFinder)
105
+ )
106
+ end
69
107
  end
70
108
  end
71
109
  end
@@ -39,6 +39,7 @@ module Dependabot
39
39
  RESOLUTION_IMPOSSIBLE_ERROR = T.let("ResolutionImpossible", String)
40
40
  ERROR_REGEX = T.let(/(?<=ERROR\:\W).*$/, Regexp)
41
41
  UV_UNRESOLVABLE_REGEX = T.let(/ × No solution found when resolving dependencies:[\s\S]*$/, Regexp)
42
+ PYTHON_VERSION_REGEX = T.let(/--python-version[=\s]+(?<version>\d+\.\d+(?:\.\d+)?)/, Regexp)
42
43
 
43
44
  sig { returns(Dependabot::Dependency) }
44
45
  attr_reader :dependency
@@ -272,6 +273,8 @@ module Dependabot
272
273
  /--index-url=\S+/, "--index-url=<index_url>"
273
274
  ).sub(
274
275
  /--extra-index-url=\S+/, "--extra-index-url=<extra_index_url>"
276
+ ).sub(
277
+ /--python-version=\S+/, "--python-version=<python_version>"
275
278
  )
276
279
  end
277
280
 
@@ -342,10 +345,19 @@ module Dependabot
342
345
 
343
346
  options << "--universal" if T.must(requirements_file.content).include?("--universal")
344
347
 
345
- options
348
+ options << extract_python_version_option(requirements_file)
349
+
350
+ options.compact
346
351
  end
347
352
  # rubocop:enable Metrics/AbcSize
348
353
 
354
+ sig { params(requirements_file: Dependabot::DependencyFile).returns(T.nilable(String)) }
355
+ def extract_python_version_option(requirements_file)
356
+ return unless (match = PYTHON_VERSION_REGEX.match(T.must(requirements_file.content)))
357
+
358
+ "--python-version=#{match[:version]}"
359
+ end
360
+
349
361
  sig { returns(T::Hash[String, String]) }
350
362
  def python_env
351
363
  env = {}
@@ -127,7 +127,12 @@ module Dependabot
127
127
  fix_version = lowest_security_fix_version
128
128
  return latest_resolvable_version if fix_version.nil?
129
129
 
130
- return resolver.lowest_resolvable_security_fix_version if resolver_type == :requirements
130
+ # For requirements and lock_file resolver types, delegate to the resolver
131
+ if resolver_type == :requirements || resolver_type == :lock_file
132
+ resolved_fix = resolver.lowest_resolvable_security_fix_version
133
+ # If no security fix version is found, fall back to latest_resolvable_version
134
+ return resolved_fix || latest_resolvable_version
135
+ end
131
136
 
132
137
  resolver.resolvable?(version: fix_version) ? fix_version : nil
133
138
  end
@@ -216,7 +221,9 @@ module Dependabot
216
221
  dependency: dependency,
217
222
  dependency_files: dependency_files,
218
223
  credentials: credentials,
219
- repo_contents_path: repo_contents_path
224
+ repo_contents_path: repo_contents_path,
225
+ security_advisories: security_advisories,
226
+ ignored_versions: ignored_versions
220
227
  ),
221
228
  T.nilable(LockFileResolver)
222
229
  )
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.351.0
4
+ version: 0.353.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.351.0
18
+ version: 0.353.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.351.0
25
+ version: 0.353.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.351.0
32
+ version: 0.353.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.351.0
39
+ version: 0.353.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -264,6 +264,7 @@ files:
264
264
  - lib/dependabot/uv.rb
265
265
  - lib/dependabot/uv/authed_url_builder.rb
266
266
  - lib/dependabot/uv/file_fetcher.rb
267
+ - lib/dependabot/uv/file_fetcher/workspace_fetcher.rb
267
268
  - lib/dependabot/uv/file_parser.rb
268
269
  - lib/dependabot/uv/file_parser/pyproject_files_parser.rb
269
270
  - lib/dependabot/uv/file_parser/python_requirement_parser.rb
@@ -282,6 +283,7 @@ files:
282
283
  - lib/dependabot/uv/package_manager.rb
283
284
  - lib/dependabot/uv/requirement.rb
284
285
  - lib/dependabot/uv/requirement_parser.rb
286
+ - lib/dependabot/uv/requirement_suffix_helper.rb
285
287
  - lib/dependabot/uv/requirements_file_matcher.rb
286
288
  - lib/dependabot/uv/update_checker.rb
287
289
  - lib/dependabot/uv/update_checker/latest_version_finder.rb
@@ -295,7 +297,7 @@ licenses:
295
297
  - MIT
296
298
  metadata:
297
299
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
298
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.351.0
300
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.353.0
299
301
  rdoc_options: []
300
302
  require_paths:
301
303
  - lib