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 +4 -4
- data/helpers/lib/parser.py +13 -5
- data/lib/dependabot/uv/file_fetcher/workspace_fetcher.rb +210 -0
- data/lib/dependabot/uv/file_fetcher.rb +18 -29
- data/lib/dependabot/uv/file_updater/compile_file_updater.rb +21 -12
- data/lib/dependabot/uv/file_updater/lock_file_updater.rb +36 -12
- data/lib/dependabot/uv/requirement_suffix_helper.rb +29 -0
- data/lib/dependabot/uv/update_checker/lock_file_resolver.rb +41 -3
- data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +13 -1
- data/lib/dependabot/uv/update_checker.rb +9 -2
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7a64a9814f7e13db97058e97650e562835fd427001c18b08d9d13ff426e0b9f
|
|
4
|
+
data.tar.gz: f19a7c8dbc8208cfdba09e5f5513b04895b6ff4eb32c5cba0690bb21e2a2db6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff7a174888fb664c4358198498c99eb6179b23c259297a8693b213609b876b8e8fd3350206e928f10bd51654c6f0a64987b28ad792dab1d72ab813d85574cae4
|
|
7
|
+
data.tar.gz: a94975fe714eae7eadac990e4ce9c8d996ac4478b0016557245fb5fc2e5745591832f2098d1ca0383bcb7de025cf0e85b047aeefed2baaa8b602e90ac4d0431f
|
data/helpers/lib/parser.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
|
123
|
-
|
|
124
|
+
def workspace_member_files
|
|
125
|
+
workspace_fetcher.workspace_member_files
|
|
126
|
+
end
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
497
|
-
options << "--generate-hashes" if
|
|
498
|
-
options << "--no-annotate" unless
|
|
499
|
-
options << "--pre" if
|
|
500
|
-
options << "--no-strip-extras" if
|
|
501
|
-
|
|
502
|
-
if
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
end
|
|
177
|
+
normalized_requirement(req1) == normalized_requirement(req2)
|
|
178
|
+
end
|
|
161
179
|
|
|
162
|
-
|
|
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
|
|
378
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|