dependabot-conda 0.349.0 → 0.350.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/conda/conda_registry_client.rb +220 -0
- data/lib/dependabot/conda/file_fetcher.rb +115 -66
- data/lib/dependabot/conda/file_parser.rb +40 -38
- data/lib/dependabot/conda/file_updater.rb +40 -16
- data/lib/dependabot/conda/metadata_finder.rb +8 -15
- data/lib/dependabot/conda/update_checker/latest_version_finder.rb +111 -6
- data/lib/dependabot/conda/update_checker/requirements_updater.rb +428 -0
- data/lib/dependabot/conda/update_checker.rb +16 -63
- data/lib/dependabot/conda/version.rb +317 -6
- data/lib/dependabot/conda.rb +4 -3
- metadata +11 -10
- data/lib/dependabot/conda/python_package_classifier.rb +0 -88
|
@@ -30,6 +30,21 @@ module Dependabot
|
|
|
30
30
|
T.proc.params(arg0: T.untyped).returns(Regexp)
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
# Bracket syntax: package[version='>=1.0']
|
|
34
|
+
CONDA_BRACKET_PATTERN = T.let(
|
|
35
|
+
lambda do |name|
|
|
36
|
+
/^(\s{2,4}-\s+)(#{Regexp.escape(name)})(\[version=['"])[^'"]+(['"]\])(\s*)(#.*)?$/
|
|
37
|
+
end,
|
|
38
|
+
T.proc.params(arg0: T.untyped).returns(Regexp)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
CONDA_CHANNEL_BRACKET_PATTERN = T.let(
|
|
42
|
+
lambda do |name|
|
|
43
|
+
/^(\s{2,4}-\s+[a-zA-Z0-9_.-]+::)(#{Regexp.escape(name)})(\[version=['"])[^'"]+(['"]\])(\s*)(#.*)?$/
|
|
44
|
+
end,
|
|
45
|
+
T.proc.params(arg0: T.untyped).returns(Regexp)
|
|
46
|
+
)
|
|
47
|
+
|
|
33
48
|
PIP_PATTERN = T.let(
|
|
34
49
|
lambda do |name|
|
|
35
50
|
/^(\s{5,}-\s+)(#{Regexp.escape(name)})#{VERSION_CONSTRAINT_PATTERN}(\s*)(#.*)?$/
|
|
@@ -43,7 +58,6 @@ module Dependabot
|
|
|
43
58
|
|
|
44
59
|
environment_files.each do |file|
|
|
45
60
|
updated_file = update_environment_file(file)
|
|
46
|
-
# Always include a file (even if unchanged) to match expected behavior
|
|
47
61
|
updated_files << (updated_file || file)
|
|
48
62
|
end
|
|
49
63
|
|
|
@@ -56,7 +70,6 @@ module Dependabot
|
|
|
56
70
|
def check_required_files
|
|
57
71
|
filenames = dependency_files.map(&:name)
|
|
58
72
|
raise "No environment.yml file found!" unless filenames.any? { |name| name.match?(/^environment\.ya?ml$/i) }
|
|
59
|
-
# File found, all good
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
@@ -140,18 +153,13 @@ module Dependabot
|
|
|
140
153
|
).returns(T::Hash[Symbol, T.untyped])
|
|
141
154
|
end
|
|
142
155
|
def update_conda_dependency_in_content(content, dependency)
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
# Enhanced to handle flexible indentation, space around operators, and comment preservation
|
|
146
|
-
# But restrict to main dependencies section (not deeply nested like pip section)
|
|
147
|
-
conda_patterns = [
|
|
148
|
-
# With channel prefix - main dependencies section (2-4 spaces to avoid pip section)
|
|
156
|
+
# Try standard patterns first (most common)
|
|
157
|
+
standard_patterns = [
|
|
149
158
|
CONDA_CHANNEL_PATTERN.call(dependency.name),
|
|
150
|
-
# Without channel prefix - main dependencies section (2-4 spaces to avoid pip section)
|
|
151
159
|
CONDA_SIMPLE_PATTERN.call(dependency.name)
|
|
152
160
|
]
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
standard_patterns.each do |pattern|
|
|
155
163
|
next unless content.match?(pattern)
|
|
156
164
|
|
|
157
165
|
updated_content = content.gsub(pattern) do
|
|
@@ -159,13 +167,34 @@ module Dependabot
|
|
|
159
167
|
name = ::Regexp.last_match(2)
|
|
160
168
|
whitespace_before_comment = ::Regexp.last_match(4) || ""
|
|
161
169
|
comment = ::Regexp.last_match(5) || ""
|
|
162
|
-
# Use the requirement from the dependency object, or default to =version
|
|
163
170
|
new_requirement = get_requirement_for_dependency(dependency, "conda")
|
|
164
171
|
"#{prefix}#{name}#{new_requirement}#{whitespace_before_comment}#{comment}"
|
|
165
172
|
end
|
|
166
173
|
return { updated: true, content: updated_content, not_found: false }
|
|
167
174
|
end
|
|
168
175
|
|
|
176
|
+
# Try bracket syntax patterns (rare cases)
|
|
177
|
+
bracket_patterns = [
|
|
178
|
+
CONDA_CHANNEL_BRACKET_PATTERN.call(dependency.name),
|
|
179
|
+
CONDA_BRACKET_PATTERN.call(dependency.name)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
bracket_patterns.each do |pattern|
|
|
183
|
+
next unless content.match?(pattern)
|
|
184
|
+
|
|
185
|
+
updated_content = content.gsub(pattern) do
|
|
186
|
+
prefix = ::Regexp.last_match(1)
|
|
187
|
+
name = ::Regexp.last_match(2)
|
|
188
|
+
bracket_start = ::Regexp.last_match(3)
|
|
189
|
+
bracket_end = ::Regexp.last_match(4)
|
|
190
|
+
whitespace_before_comment = ::Regexp.last_match(5) || ""
|
|
191
|
+
comment = ::Regexp.last_match(6) || ""
|
|
192
|
+
new_requirement = get_requirement_for_dependency(dependency, "conda")
|
|
193
|
+
"#{prefix}#{name}#{bracket_start}#{new_requirement}#{bracket_end}#{whitespace_before_comment}#{comment}"
|
|
194
|
+
end
|
|
195
|
+
return { updated: true, content: updated_content, not_found: false }
|
|
196
|
+
end
|
|
197
|
+
|
|
169
198
|
{ updated: false, content: content, not_found: false }
|
|
170
199
|
end
|
|
171
200
|
|
|
@@ -176,10 +205,6 @@ module Dependabot
|
|
|
176
205
|
).returns(T::Hash[Symbol, T.untyped])
|
|
177
206
|
end
|
|
178
207
|
def update_pip_dependency_in_content(content, dependency)
|
|
179
|
-
# Pattern to match pip dependency lines in pip section
|
|
180
|
-
# Enhanced to handle flexible indentation for pip section (5+ spaces to distinguish from main deps),
|
|
181
|
-
# better operator support, and multiple constraints like "requests>=2.25.0,<3.0"
|
|
182
|
-
# Capture whitespace between requirement and comment to preserve formatting
|
|
183
208
|
pip_pattern = PIP_PATTERN.call(dependency.name)
|
|
184
209
|
|
|
185
210
|
if content.match?(pip_pattern)
|
|
@@ -188,7 +213,6 @@ module Dependabot
|
|
|
188
213
|
name = ::Regexp.last_match(2)
|
|
189
214
|
whitespace_before_comment = ::Regexp.last_match(4) || ""
|
|
190
215
|
comment = ::Regexp.last_match(5) || ""
|
|
191
|
-
# Use the requirement from the dependency object, or default to ==version
|
|
192
216
|
new_requirement = get_requirement_for_dependency(dependency, "pip")
|
|
193
217
|
"#{prefix}#{name}#{new_requirement}#{whitespace_before_comment}#{comment}"
|
|
194
218
|
end
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
require "dependabot/metadata_finders"
|
|
6
6
|
require "dependabot/metadata_finders/base"
|
|
7
|
-
require "dependabot/conda/python_package_classifier"
|
|
8
7
|
require "dependabot/python/metadata_finder"
|
|
9
8
|
|
|
10
9
|
module Dependabot
|
|
@@ -14,32 +13,27 @@ module Dependabot
|
|
|
14
13
|
|
|
15
14
|
sig { override.returns(T.nilable(String)) }
|
|
16
15
|
def homepage_url
|
|
17
|
-
return
|
|
16
|
+
return python_metadata_finder.homepage_url if pip_dependency?
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
python_metadata_finder.homepage_url
|
|
18
|
+
nil
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
private
|
|
24
22
|
|
|
25
23
|
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
26
24
|
def look_up_source
|
|
27
|
-
return
|
|
25
|
+
return python_metadata_finder.send(:look_up_source) if pip_dependency?
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
python_metadata_finder.send(:look_up_source)
|
|
27
|
+
nil
|
|
31
28
|
end
|
|
32
29
|
|
|
33
|
-
sig {
|
|
34
|
-
def
|
|
35
|
-
|
|
30
|
+
sig { returns(T::Boolean) }
|
|
31
|
+
def pip_dependency?
|
|
32
|
+
dependency.requirements.any? { |req| req[:groups]&.include?("pip") }
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
sig { returns(Dependabot::Python::MetadataFinder) }
|
|
39
36
|
def python_metadata_finder
|
|
40
|
-
# Cache the Python metadata finder instance for reuse across method calls
|
|
41
|
-
# Credentials are passed through as-is since conda manifests don't specify pip-index credentials
|
|
42
|
-
# TODO: If we decide to support non python packages for Conda we will have to review this
|
|
43
37
|
@python_metadata_finder ||= T.let(
|
|
44
38
|
Dependabot::Python::MetadataFinder.new(
|
|
45
39
|
dependency: python_dependency,
|
|
@@ -57,8 +51,7 @@ module Dependabot
|
|
|
57
51
|
version: dependency.version,
|
|
58
52
|
requirements: dependency.requirements.map do |req|
|
|
59
53
|
req.merge(
|
|
60
|
-
|
|
61
|
-
source: nil # No private pip-index in conda manifests
|
|
54
|
+
source: nil
|
|
62
55
|
)
|
|
63
56
|
end,
|
|
64
57
|
package_manager: "pip"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "yaml"
|
|
4
5
|
require "sorbet-runtime"
|
|
5
6
|
require "dependabot/package/package_latest_version_finder"
|
|
7
|
+
require "dependabot/package/package_release"
|
|
6
8
|
require "dependabot/python/update_checker/latest_version_finder"
|
|
7
9
|
require "dependabot/dependency"
|
|
10
|
+
require "dependabot/conda/conda_registry_client"
|
|
8
11
|
require_relative "requirement_translator"
|
|
9
12
|
|
|
10
13
|
module Dependabot
|
|
@@ -35,13 +38,18 @@ module Dependabot
|
|
|
35
38
|
)
|
|
36
39
|
@raise_on_ignored = T.let(raise_on_ignored, T::Boolean)
|
|
37
40
|
@cooldown_options = T.let(cooldown_options, T.nilable(Dependabot::Package::ReleaseCooldownOptions))
|
|
41
|
+
@conda_client = T.let(CondaRegistryClient.new, CondaRegistryClient)
|
|
38
42
|
|
|
39
43
|
super
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) }
|
|
43
47
|
def package_details
|
|
44
|
-
@package_details ||=
|
|
48
|
+
@package_details ||= if pip_dependency?
|
|
49
|
+
python_latest_version_finder.package_details
|
|
50
|
+
else
|
|
51
|
+
conda_package_details
|
|
52
|
+
end
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
sig { override.returns(T::Boolean) }
|
|
@@ -51,6 +59,106 @@ module Dependabot
|
|
|
51
59
|
|
|
52
60
|
private
|
|
53
61
|
|
|
62
|
+
sig { returns(T::Boolean) }
|
|
63
|
+
def pip_dependency?
|
|
64
|
+
dependency.requirements.any? { |req| req[:groups]&.include?("pip") }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
sig { returns(T.nilable(Dependabot::Package::PackageDetails)) }
|
|
68
|
+
def conda_package_details
|
|
69
|
+
channels_to_search.each do |channel|
|
|
70
|
+
versions = @conda_client.available_versions(dependency.name, channel)
|
|
71
|
+
next if versions.empty?
|
|
72
|
+
|
|
73
|
+
releases = versions.map.with_index do |version, index|
|
|
74
|
+
Dependabot::Package::PackageRelease.new(
|
|
75
|
+
version: version,
|
|
76
|
+
url: "https://anaconda.org/#{channel}/#{dependency.name}",
|
|
77
|
+
latest: index.zero?
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return Dependabot::Package::PackageDetails.new(
|
|
82
|
+
dependency: dependency,
|
|
83
|
+
releases: releases
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
sig { returns(T::Array[String]) }
|
|
91
|
+
def channels_to_search
|
|
92
|
+
channels = []
|
|
93
|
+
|
|
94
|
+
# Priority 1: Explicit source channel
|
|
95
|
+
source_channel = extract_channel_from_source
|
|
96
|
+
channels << source_channel if source_channel
|
|
97
|
+
|
|
98
|
+
# Priority 2: Channel prefix in requirement
|
|
99
|
+
requirement_channel = extract_channel_from_requirement
|
|
100
|
+
channels << requirement_channel if requirement_channel && !channels.include?(requirement_channel)
|
|
101
|
+
|
|
102
|
+
# Priority 3: Environment file channels
|
|
103
|
+
env_channels = extract_all_channels_from_environment_file
|
|
104
|
+
env_channels.each do |ch|
|
|
105
|
+
channels << ch unless channels.include?(ch)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Priority 4: Default fallback channels
|
|
109
|
+
[CondaRegistryClient::DEFAULT_CHANNEL, "conda-forge", "main"].each do |ch|
|
|
110
|
+
channels << ch unless channels.include?(ch)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
channels
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
sig { returns(T.nilable(String)) }
|
|
117
|
+
def extract_channel_from_source
|
|
118
|
+
return nil unless dependency.requirements.first
|
|
119
|
+
|
|
120
|
+
source = T.let(T.must(dependency.requirements.first)[:source], T.nilable(T::Hash[Symbol, T.untyped]))
|
|
121
|
+
return nil unless source
|
|
122
|
+
|
|
123
|
+
channel = source[:channel]
|
|
124
|
+
return nil unless channel.is_a?(String)
|
|
125
|
+
return nil unless CondaRegistryClient::SUPPORTED_CHANNELS.include?(channel)
|
|
126
|
+
|
|
127
|
+
channel
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { returns(T.nilable(String)) }
|
|
131
|
+
def extract_channel_from_requirement
|
|
132
|
+
dependency.requirements.each do |req|
|
|
133
|
+
requirement_string = req[:requirement]
|
|
134
|
+
next unless requirement_string&.include?("::")
|
|
135
|
+
|
|
136
|
+
channel = requirement_string.split("::").first
|
|
137
|
+
next unless channel
|
|
138
|
+
next unless CondaRegistryClient::SUPPORTED_CHANNELS.include?(channel)
|
|
139
|
+
|
|
140
|
+
return channel
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { returns(T::Array[String]) }
|
|
147
|
+
def extract_all_channels_from_environment_file
|
|
148
|
+
environment_file = dependency_files.find { |f| f.name.match?(/environment\.ya?ml/i) }
|
|
149
|
+
return [] unless environment_file
|
|
150
|
+
|
|
151
|
+
parsed = YAML.safe_load(T.must(environment_file.content))
|
|
152
|
+
return [] unless parsed.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
channels = parsed["channels"]
|
|
155
|
+
return [] unless channels.is_a?(Array)
|
|
156
|
+
|
|
157
|
+
channels.select { |ch| ch.is_a?(String) && CondaRegistryClient::SUPPORTED_CHANNELS.include?(ch) }
|
|
158
|
+
rescue Psych::SyntaxError
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
|
|
54
162
|
sig { returns(Dependabot::Python::UpdateChecker::LatestVersionFinder) }
|
|
55
163
|
def python_latest_version_finder
|
|
56
164
|
@python_latest_version_finder ||= T.let(
|
|
@@ -69,12 +177,11 @@ module Dependabot
|
|
|
69
177
|
|
|
70
178
|
sig { returns(Dependabot::Dependency) }
|
|
71
179
|
def python_compatible_dependency
|
|
72
|
-
# Convert conda dependency to python-compatible dependency
|
|
73
180
|
Dependabot::Dependency.new(
|
|
74
181
|
name: dependency.name,
|
|
75
182
|
version: dependency.version,
|
|
76
183
|
requirements: python_compatible_requirements,
|
|
77
|
-
package_manager: "pip"
|
|
184
|
+
package_manager: "pip"
|
|
78
185
|
)
|
|
79
186
|
end
|
|
80
187
|
|
|
@@ -90,7 +197,6 @@ module Dependabot
|
|
|
90
197
|
sig { returns(T::Array[Dependabot::SecurityAdvisory]) }
|
|
91
198
|
def python_compatible_security_advisories
|
|
92
199
|
security_advisories.map do |advisory|
|
|
93
|
-
# Convert Conda requirements to Python requirements for pip compatibility
|
|
94
200
|
python_vulnerable_versions = advisory.vulnerable_versions.flat_map do |conda_req|
|
|
95
201
|
Dependabot::Python::Requirement.requirements_array(conda_req.to_s)
|
|
96
202
|
end
|
|
@@ -99,10 +205,9 @@ module Dependabot
|
|
|
99
205
|
Dependabot::Python::Requirement.requirements_array(conda_req.to_s)
|
|
100
206
|
end
|
|
101
207
|
|
|
102
|
-
# Normalize security advisories to use 'pip' package manager for Python delegation
|
|
103
208
|
Dependabot::SecurityAdvisory.new(
|
|
104
209
|
dependency_name: advisory.dependency_name,
|
|
105
|
-
package_manager: "pip",
|
|
210
|
+
package_manager: "pip",
|
|
106
211
|
vulnerable_versions: python_vulnerable_versions,
|
|
107
212
|
safe_versions: python_safe_versions
|
|
108
213
|
)
|