dependabot-conda 0.349.0 → 0.351.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.
@@ -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
- # Pattern to match conda dependency lines (with optional channel prefix)
144
- # Matches: " - numpy=1.26", " - conda-forge::numpy>=1.21.0", " - numpy >= 1.21.0 # comment", etc.
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
- conda_patterns.each do |pattern|
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 super unless python_package?(dependency.name)
16
+ return python_metadata_finder.homepage_url if pip_dependency?
18
17
 
19
- # Delegate to Python metadata finder for enhanced PyPI-based homepage URLs
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 nil unless python_package?(dependency.name)
25
+ return python_metadata_finder.send(:look_up_source) if pip_dependency?
28
26
 
29
- # Delegate to Python metadata finder for Python packages
30
- python_metadata_finder.send(:look_up_source)
27
+ nil
31
28
  end
32
29
 
33
- sig { params(package_name: String).returns(T::Boolean) }
34
- def python_package?(package_name)
35
- PythonPackageClassifier.python_package?(package_name)
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
- file: req[:file]&.gsub(/environment\.ya?ml/, "requirements.txt"),
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 ||= python_latest_version_finder.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" # Use pip for PyPI compatibility
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", # Use pip for PyPI compatibility
210
+ package_manager: "pip",
106
211
  vulnerable_versions: python_vulnerable_versions,
107
212
  safe_versions: python_safe_versions
108
213
  )