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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d533b2ab537726987e4a367cd0aeeea8f8567edcd218186bf74f2b0075db10e
4
- data.tar.gz: 1ec1bed1c8a656534c97e20cbc4ed48ec54f15ecc244bded4adeb1fe21f873c4
3
+ metadata.gz: ce8cbd205c7b0ab1f739bfa46499f627230e6fe1628a7da150b136dcd7302658
4
+ data.tar.gz: feaa389e0e1332309d6bba5b583bb0933bcdb389348810b77200c6d62e279c65
5
5
  SHA512:
6
- metadata.gz: af2b6c51628577e8af809b2d6b540b93d0ef7d8d2036265c0e6766908ea906d5b048586daf71eec920e4e2ea3172b39069a1a26ef60b5e50eb6530e56893e057
7
- data.tar.gz: 14ce274e25f589a443c28c8fc762688174faab1a68bd038a05ad2007795c246636f6e2d068f4a771c776ef0aeba29b1efabe9974623dd8ffbd1c22f7e2294b61
6
+ metadata.gz: aac66701b1fa4423be74d27af6a806bee85103145a07ce076d1b00238858b696cb62a6ca9b6bcb299100e936cda55d12d3477b4ce7927eaa76675e88be1d2ab8
7
+ data.tar.gz: 2480678c876f4a278fa313f0687f6b0d6cd6d8823f3e9bcb71fc422c98d8eb97240fba624b2a0c12de7df1977fee0c6cf7a4cd42ec24271b9e5115d75c764aaa
@@ -0,0 +1,220 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "excon"
5
+ require "json"
6
+ require "sorbet-runtime"
7
+ require "dependabot/conda/version"
8
+ require "dependabot/registry_client"
9
+ require "dependabot/shared_helpers"
10
+
11
+ module Dependabot
12
+ module Conda
13
+ class CondaRegistryClient
14
+ extend T::Sig
15
+
16
+ # Supported public conda channels (user-facing names from environment.yml)
17
+ SUPPORTED_CHANNELS = T.let(
18
+ %w(anaconda conda-forge defaults bioconda main).freeze,
19
+ T::Array[String]
20
+ )
21
+
22
+ # Channel aliases: maps user-facing channel names to API channel names
23
+ # 'defaults' is a Conda client alias that doesn't exist on anaconda.org API
24
+ CHANNEL_ALIASES = T.let(
25
+ { "defaults" => "anaconda" }.freeze,
26
+ T::Hash[String, String]
27
+ )
28
+ # anaconda.org API configuration
29
+ DEFAULT_CHANNEL = T.let("anaconda", String)
30
+ API_BASE_URL = T.let("https://api.anaconda.org", String)
31
+ CONNECTION_TIMEOUT = T.let(5, Integer)
32
+ READ_TIMEOUT = T.let(10, Integer)
33
+ MAX_RETRIES = T.let(1, Integer)
34
+
35
+ sig { void }
36
+ def initialize
37
+ @cache = T.let({}, T::Hash[String, T.untyped])
38
+ @not_found_cache = T.let(Set.new, T::Set[String])
39
+ end
40
+
41
+ # Fetch package metadata from Conda API
42
+ sig do
43
+ params(
44
+ package_name: String,
45
+ channel: String
46
+ ).returns(T.nilable(T::Hash[String, T.untyped]))
47
+ end
48
+ def fetch_package_metadata(package_name, channel = DEFAULT_CHANNEL)
49
+ cache_key = "#{channel}/#{package_name}"
50
+
51
+ # Check 404 cache first
52
+ return nil if @not_found_cache.include?(cache_key)
53
+
54
+ # Check cache
55
+ return @cache[cache_key] if @cache.key?(cache_key)
56
+
57
+ # Fetch from API
58
+ fetch_from_api(package_name, channel, cache_key)
59
+ end
60
+
61
+ # Check if a specific version exists for a package
62
+ sig do
63
+ params(
64
+ package_name: String,
65
+ version: String,
66
+ channel: String
67
+ ).returns(T::Boolean)
68
+ end
69
+ def version_exists?(package_name, version, channel = DEFAULT_CHANNEL)
70
+ metadata = fetch_package_metadata(package_name, channel)
71
+ return false unless metadata
72
+
73
+ versions = metadata["versions"]
74
+ return false unless versions.is_a?(Array)
75
+
76
+ versions.include?(version)
77
+ end
78
+
79
+ # Get all available versions for a package, sorted newest first
80
+ sig do
81
+ params(
82
+ package_name: String,
83
+ channel: String
84
+ ).returns(T::Array[Dependabot::Conda::Version])
85
+ end
86
+ def available_versions(package_name, channel = DEFAULT_CHANNEL)
87
+ metadata = fetch_package_metadata(package_name, channel)
88
+ return [] unless metadata
89
+
90
+ versions = metadata["versions"]
91
+ return [] unless versions.is_a?(Array)
92
+
93
+ # Parse and sort versions
94
+ parsed_versions = versions.filter_map do |version_string|
95
+ Dependabot::Conda::Version.new(version_string)
96
+ rescue ArgumentError
97
+ # Invalid version format - skip it
98
+ Dependabot.logger.debug("Skipping invalid conda version: #{version_string}")
99
+ nil
100
+ end
101
+
102
+ # Sort newest first
103
+ parsed_versions.sort.reverse
104
+ end
105
+
106
+ # Get the latest version for a package
107
+ sig do
108
+ params(
109
+ package_name: String,
110
+ channel: String
111
+ ).returns(T.nilable(Dependabot::Conda::Version))
112
+ end
113
+ def latest_version(package_name, channel = DEFAULT_CHANNEL)
114
+ versions = available_versions(package_name, channel)
115
+ versions.first
116
+ end
117
+
118
+ # Get package metadata fields for MetadataFinder
119
+ sig do
120
+ params(
121
+ package_name: String,
122
+ channel: String
123
+ ).returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
124
+ end
125
+ def package_metadata(package_name, channel = DEFAULT_CHANNEL)
126
+ metadata = fetch_package_metadata(package_name, channel)
127
+ return nil unless metadata
128
+
129
+ {
130
+ homepage: metadata["home"],
131
+ source_url: metadata["dev_url"],
132
+ description: metadata["summary"],
133
+ license: metadata["license"]
134
+ }
135
+ end
136
+
137
+ private
138
+
139
+ # Normalize channel name from user-facing to API-compatible
140
+ sig { params(channel: String).returns(String) }
141
+ def normalize_channel(channel)
142
+ CHANNEL_ALIASES[channel] || channel
143
+ end
144
+
145
+ sig do
146
+ params(
147
+ package_name: String,
148
+ channel: String,
149
+ cache_key: String
150
+ ).returns(T.nilable(T::Hash[String, T.untyped]))
151
+ end
152
+ def fetch_from_api(package_name, channel, cache_key)
153
+ # Normalize channel name for API (e.g., 'defaults' -> 'anaconda')
154
+ api_channel = normalize_channel(channel)
155
+ url = "#{API_BASE_URL}/package/#{api_channel}/#{package_name}"
156
+
157
+ begin
158
+ response = make_http_request(url)
159
+ handle_response(response, package_name, cache_key)
160
+ rescue JSON::ParserError => e
161
+ Dependabot.logger.error("Invalid JSON from Conda API for #{package_name}: #{e.message}")
162
+ nil
163
+ rescue Excon::Error::Socket, Excon::Error::Timeout => e
164
+ Dependabot.logger.error("Conda API connection error for #{package_name}: #{e.message}")
165
+ raise Dependabot::DependabotError, "Failed to connect to Conda API: #{e.message}"
166
+ end
167
+ end
168
+
169
+ sig { params(url: String).returns(Excon::Response) }
170
+ def make_http_request(url)
171
+ Dependabot::RegistryClient.get(
172
+ url: url,
173
+ headers: {
174
+ "Accept" => "application/json",
175
+ "User-Agent" => Dependabot::SharedHelpers::USER_AGENT
176
+ },
177
+ options: {
178
+ connect_timeout: CONNECTION_TIMEOUT,
179
+ read_timeout: READ_TIMEOUT,
180
+ retry_limit: MAX_RETRIES
181
+ }
182
+ )
183
+ end
184
+
185
+ sig do
186
+ params(
187
+ response: Excon::Response,
188
+ package_name: String,
189
+ cache_key: String
190
+ ).returns(T.nilable(T::Hash[String, T.untyped]))
191
+ end
192
+ def handle_response(response, package_name, cache_key)
193
+ case response.status
194
+ when 200
195
+ data = JSON.parse(response.body)
196
+ @cache[cache_key] = data
197
+ data
198
+ when 404
199
+ @not_found_cache.add(cache_key)
200
+ nil
201
+ when 429
202
+ handle_rate_limit(response, package_name)
203
+ else
204
+ Dependabot.logger.error("Unexpected Conda API response: #{response.status} for #{package_name}")
205
+ nil
206
+ end
207
+ end
208
+
209
+ sig { params(response: Excon::Response, package_name: String).returns(T.noreturn) }
210
+ def handle_rate_limit(response, package_name)
211
+ retry_after = response.headers["Retry-After"]&.to_i || 60
212
+ Dependabot.logger.warn(
213
+ "Conda API rate limited. Retry after #{retry_after} seconds. Package: #{package_name}"
214
+ )
215
+ raise Dependabot::DependabotError,
216
+ "Conda API rate limited. Please try again in #{retry_after} seconds."
217
+ end
218
+ end
219
+ end
220
+ end
@@ -1,10 +1,10 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "yaml"
4
5
  require "sorbet-runtime"
5
6
  require "dependabot/file_fetchers"
6
7
  require "dependabot/file_fetchers/base"
7
- require "dependabot/conda/python_package_classifier"
8
8
 
9
9
  module Dependabot
10
10
  module Conda
@@ -31,101 +31,150 @@ module Dependabot
31
31
 
32
32
  sig { override.returns(T::Array[DependencyFile]) }
33
33
  def fetch_files
34
- []
34
+ unless allow_beta_ecosystems?
35
+ raise Dependabot::DependencyFileNotFound.new(
36
+ nil,
37
+ "Conda support is currently in beta. Set ALLOW_BETA_ECOSYSTEMS=true to enable it."
38
+ )
39
+ end
40
+
41
+ fetched_files = []
42
+
43
+ # Try to fetch environment.yml first, then environment.yaml
44
+ environment_file = fetch_file_if_present("environment.yml") ||
45
+ fetch_file_if_present("environment.yaml")
46
+
47
+ if environment_file
48
+ # Validate it's a proper conda environment file
49
+ validation = validate_conda_environment(environment_file)
50
+ unless validation[:valid]
51
+ raise(
52
+ Dependabot::DependencyFileNotFound.new(
53
+ File.join(directory, environment_file.name),
54
+ unsupported_environment_message(validation[:reason])
55
+ )
56
+ )
57
+ end
58
+ fetched_files << environment_file
59
+ end
60
+
61
+ return fetched_files if fetched_files.any?
62
+
63
+ raise(
64
+ Dependabot::DependencyFileNotFound,
65
+ File.join(directory, "environment.yml")
66
+ )
35
67
  end
36
68
 
37
69
  private
38
70
 
39
- # Check if an environment file contains Python packages we can manage
40
- sig { params(file: DependencyFile).returns(T::Boolean) }
41
- def environment_contains_manageable_packages?(file)
71
+ # Validate that environment file is a proper conda manifest with manageable packages
72
+ # Returns a hash with :valid (Boolean) and :reason (Symbol or nil)
73
+ sig { params(file: DependencyFile).returns(T::Hash[Symbol, T.untyped]) }
74
+ def validate_conda_environment(file)
42
75
  content = file.content
43
- return false unless content
76
+ return { valid: false, reason: :no_content } unless content
44
77
 
45
- parsed_yaml = begin
46
- parse_yaml_content(content)
47
- rescue Psych::SyntaxError => e
48
- Dependabot.logger.error("YAML parsing error: #{e.message}")
49
- nil
50
- end
51
- return false unless parsed_yaml
78
+ parsed_yaml = parse_and_validate_yaml(content)
79
+ return { valid: false, reason: :invalid_yaml } unless parsed_yaml
80
+
81
+ dependencies = parsed_yaml["dependencies"]
82
+ return { valid: false, reason: :no_dependencies } unless dependencies.is_a?(Array)
83
+ return { valid: false, reason: :empty_dependencies } if dependencies.empty?
52
84
 
53
- manageable_conda_packages?(parsed_yaml) || manageable_pip_packages?(parsed_yaml)
85
+ # Check if all packages are fully qualified (no manageable packages)
86
+ return { valid: false, reason: :all_fully_qualified } unless manageable_packages?(dependencies)
87
+
88
+ { valid: true, reason: nil }
54
89
  end
55
90
 
56
- # Parse YAML content and return parsed hash or nil
57
91
  sig { params(content: String).returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
58
- def parse_yaml_content(content)
59
- require "yaml"
60
- parsed = YAML.safe_load(content)
61
- parsed.is_a?(Hash) ? parsed : nil
92
+ def parse_and_validate_yaml(content)
93
+ parsed_yaml = parse_yaml_content(content)
94
+ return nil unless parsed_yaml
95
+ return nil unless parsed_yaml.is_a?(Hash)
96
+
97
+ parsed_yaml
98
+ rescue Psych::SyntaxError => e
99
+ Dependabot.logger.error("YAML parsing error: #{e.message}")
100
+ nil
62
101
  end
63
102
 
64
- # Check if the parsed YAML contains manageable conda packages
65
- sig { params(parsed_yaml: T::Hash[T.untyped, T.untyped]).returns(T::Boolean) }
66
- def manageable_conda_packages?(parsed_yaml)
67
- dependencies = parsed_yaml["dependencies"]
103
+ # Check if there are any manageable packages (simple specs or pip)
104
+ sig { params(dependencies: T.untyped).returns(T::Boolean) }
105
+ def manageable_packages?(dependencies)
68
106
  return false unless dependencies.is_a?(Array)
69
107
 
70
- simplified_packages = dependencies.select do |dep|
71
- dep.is_a?(String) && !fully_qualified_spec?(dep) &&
72
- PythonPackageClassifier.python_package?(PythonPackageClassifier.extract_package_name(dep))
108
+ has_simple_conda = dependencies.any? do |dep|
109
+ dep.is_a?(String) && !fully_qualified_spec?(dep)
73
110
  end
74
- simplified_packages.any?
75
- end
76
111
 
77
- # Check if the parsed YAML contains manageable pip packages
78
- sig { params(parsed_yaml: T::Hash[T.untyped, T.untyped]).returns(T::Boolean) }
79
- def manageable_pip_packages?(parsed_yaml)
80
- dependencies = parsed_yaml["dependencies"]
81
- return false unless dependencies.is_a?(Array)
112
+ has_pip = dependencies.any? { |dep| dep.is_a?(Hash) && dep.key?("pip") }
82
113
 
83
- pip_deps = dependencies.find { |dep| dep.is_a?(Hash) && dep.key?("pip") }
84
- return false unless pip_deps && pip_deps["pip"].is_a?(Array)
114
+ has_simple_conda || has_pip
115
+ end
85
116
 
86
- python_pip_packages = pip_deps["pip"].select do |pip_dep|
87
- pip_dep.is_a?(String) &&
88
- PythonPackageClassifier.python_package?(PythonPackageClassifier.extract_package_name(pip_dep))
89
- end
90
- python_pip_packages.any?
117
+ sig { params(content: String).returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
118
+ def parse_yaml_content(content)
119
+ parsed = YAML.safe_load(content)
120
+ parsed.is_a?(Hash) ? parsed : nil
91
121
  end
92
122
 
93
- # Check if a package specification is fully qualified (build string included)
94
123
  sig { params(spec: String).returns(T::Boolean) }
95
124
  def fully_qualified_spec?(spec)
96
- # Fully qualified specs have format: package=version=build_string
97
- # e.g., "numpy=1.21.0=py39h20f2e39_0"
125
+ # Fully qualified: package=version=build_string (e.g., numpy=1.21.0=py39h20f2e39_0)
126
+ return false if spec.include?("==")
127
+ return false if spec.include?("[")
128
+
98
129
  parts = spec.split("=")
99
130
  return false unless parts.length >= 3
100
131
 
101
132
  build_string = parts[2]
102
- return false unless build_string
133
+ return false unless build_string && !build_string.empty?
103
134
 
104
135
  build_string.match?(/^[a-zA-Z0-9_]+$/)
105
136
  end
106
137
 
107
- sig { returns(String) }
108
- def unsupported_environment_message
109
- <<~MSG
110
- This Conda environment file is not currently supported by Dependabot.
111
-
112
- Dependabot-Conda supports Python packages only and requires one of the following:
113
-
114
- 1. **Simplified conda specifications**: Dependencies using simple version syntax (e.g., numpy=1.21.0)
115
- 2. **Pip section with Python packages**: A 'pip:' section containing Python packages from PyPI
116
-
117
- **Not supported:**
118
- - Fully qualified conda specifications (e.g., numpy=1.21.0=py39h20f2e39_0)
119
- - Non-Python packages (R packages, system tools, etc.)
120
- - Environments without any Python packages
121
-
122
- To make your environment compatible:
123
- - Use simplified conda package specifications for conda packages
124
- - Add a pip section for PyPI packages
125
- - Focus on Python packages only
126
-
127
- For more information, see the Dependabot-Conda documentation.
128
- MSG
138
+ sig { params(reason: T.nilable(Symbol)).returns(String) }
139
+ def unsupported_environment_message(reason)
140
+ case reason
141
+ when :all_fully_qualified
142
+ <<~MSG
143
+ This environment file contains only fully qualified package specifications with build strings.
144
+
145
+ Dependabot cannot update packages with build strings like:
146
+ - numpy=1.21.0=py39h20f2e39_0
147
+
148
+ To fix, remove the build string. Dependabot supports simplified specifications \
149
+ (e.g., numpy=1.21.0,r-base>=4.0)
150
+ MSG
151
+ when :no_dependencies, :empty_dependencies
152
+ <<~MSG
153
+ This environment file has no dependencies to manage.
154
+
155
+ Add at least one package to the dependencies section:
156
+ dependencies:
157
+ - python>=3.9
158
+ - numpy>=1.21.0
159
+ MSG
160
+ when :invalid_yaml
161
+ <<~MSG
162
+ This environment file contains invalid YAML syntax.
163
+
164
+ Please fix the YAML syntax errors before Dependabot can process this file.
165
+ MSG
166
+ else
167
+ <<~MSG
168
+ This Conda environment file is not supported by Dependabot.
169
+
170
+ Dependabot supports:
171
+ - Simplified conda specifications (e.g., numpy=1.21.0, r-base>=4.0)
172
+ - Pip dependencies in the pip section
173
+
174
+ Not supported:
175
+ - Fully qualified specifications with build strings (e.g., numpy=1.21.0=py39h20f2e39_0)
176
+ MSG
177
+ end
129
178
  end
130
179
  end
131
180
  end
@@ -5,10 +5,10 @@ require "yaml"
5
5
  require "sorbet-runtime"
6
6
  require "dependabot/file_parsers"
7
7
  require "dependabot/file_parsers/base"
8
- require "dependabot/conda/python_package_classifier"
9
8
  require "dependabot/conda/requirement"
10
9
  require "dependabot/conda/version"
11
10
  require "dependabot/conda/package_manager"
11
+ require "dependabot/conda/conda_registry_client"
12
12
 
13
13
  module Dependabot
14
14
  module Conda
@@ -93,15 +93,12 @@ module Dependabot
93
93
 
94
94
  dependencies.each do |dep|
95
95
  next unless dep.is_a?(String)
96
- next if dep.is_a?(Hash) # Skip pip section
97
-
98
- # Skip conda dependencies if we have fully qualified packages (Tier 2 support)
96
+ next if dep.is_a?(Hash)
99
97
  next if has_fully_qualified
100
98
 
101
99
  parsed_dep = parse_conda_dependency_string(dep, file)
102
100
  next unless parsed_dep
103
- next unless python_package?(parsed_dep[:name])
104
- next if parsed_dep[:name] == "pip" # Skip pip itself as it's infrastructure
101
+ next if parsed_dep[:name] == "pip"
105
102
 
106
103
  parsed_dependencies << create_dependency(
107
104
  name: parsed_dep[:name],
@@ -114,7 +111,7 @@ module Dependabot
114
111
  parsed_dependencies
115
112
  end
116
113
 
117
- sig { params(dependencies: T.nilable(T::Array[T.untyped])).returns(T.nilable(T::Array[String])) }
114
+ sig { params(dependencies: T.untyped).returns(T.nilable(T::Array[String])) }
118
115
  def find_pip_dependencies(dependencies)
119
116
  return nil unless dependencies.is_a?(Array)
120
117
 
@@ -141,7 +138,7 @@ module Dependabot
141
138
  name: parsed_dep[:name],
142
139
  version: parsed_dep[:version],
143
140
  requirements: parsed_dep[:requirements],
144
- package_manager: "conda"
141
+ package_manager: "pip"
145
142
  )
146
143
  end
147
144
 
@@ -154,12 +151,18 @@ module Dependabot
154
151
  def parse_conda_dependency_string(dep_string, file)
155
152
  return nil if dep_string.nil?
156
153
 
154
+ # Extract channel prefix before normalizing (e.g., "conda-forge::numpy=1.26.0")
155
+ channel = extract_channel_from_dependency_string(dep_string)
156
+
157
157
  # Handle channel specifications: conda-forge::numpy=1.21.0
158
158
  normalized_dep_string = normalize_conda_dependency_string(dep_string)
159
159
  return nil if normalized_dep_string.nil?
160
160
 
161
- # Parse conda-style version specifications
162
- # Examples: numpy=1.21.0, scipy>=1.7.0, pandas, python=3.9, python>=3.8,<3.11
161
+ # Handle bracket syntax: package[version='>=1.0']
162
+ if normalized_dep_string.include?("[")
163
+ bracket_match = normalized_dep_string.match(/^([a-zA-Z0-9_.-]+)\[version=['"](.+)['"]\]$/)
164
+ normalized_dep_string = "#{bracket_match[1]}#{bracket_match[2]}" if bracket_match
165
+ end
163
166
  match = normalized_dep_string.match(/^([a-zA-Z0-9_.-]+)(?:\s*(.+))?$/)
164
167
  return nil unless match
165
168
 
@@ -167,7 +170,7 @@ module Dependabot
167
170
  constraint = match[2]&.strip
168
171
 
169
172
  version = extract_conda_version(constraint)
170
- requirements = build_conda_requirements(constraint, file)
173
+ requirements = build_conda_requirements(constraint, file, channel)
171
174
 
172
175
  {
173
176
  name: name,
@@ -176,6 +179,17 @@ module Dependabot
176
179
  }
177
180
  end
178
181
 
182
+ sig { params(dep_string: String).returns(T.nilable(String)) }
183
+ def extract_channel_from_dependency_string(dep_string)
184
+ return nil unless dep_string.include?("::")
185
+
186
+ channel = dep_string.split("::", 2).first
187
+ return nil unless channel
188
+ return nil unless CondaRegistryClient::SUPPORTED_CHANNELS.include?(channel)
189
+
190
+ channel
191
+ end
192
+
179
193
  sig { params(dep_string: String).returns(T.nilable(String)) }
180
194
  def normalize_conda_dependency_string(dep_string)
181
195
  return dep_string unless dep_string.include?("::")
@@ -189,32 +203,31 @@ module Dependabot
189
203
  return nil unless constraint
190
204
 
191
205
  case constraint
206
+ when /^==([0-9][a-zA-Z0-9._+-]+)$/
207
+ constraint[2..-1]
192
208
  when /^=([0-9][a-zA-Z0-9._+-]+)$/
193
- # Exact conda version: =1.26.0
194
- constraint[1..-1] # Remove the = prefix
209
+ constraint[1..-1]
195
210
  when /^>=([0-9][a-zA-Z0-9._+-]+)$/
196
- # Minimum version constraint: >=1.26.0
197
- # For security purposes, treat this as the current version
198
- constraint[2..-1] # Remove the >= prefix
211
+ constraint[2..-1]
199
212
  when /^~=([0-9][a-zA-Z0-9._+-]+)$/
200
- # Compatible release: ~=1.26.0
201
- constraint[2..-1] # Remove the ~= prefix
213
+ constraint[2..-1]
202
214
  end
203
215
  end
204
216
 
205
217
  sig do
206
218
  params(
207
219
  constraint: T.nilable(String),
208
- file: Dependabot::DependencyFile
220
+ file: Dependabot::DependencyFile,
221
+ channel: T.nilable(String)
209
222
  ).returns(T::Array[T::Hash[Symbol, T.untyped]])
210
223
  end
211
- def build_conda_requirements(constraint, file)
212
- return [] unless constraint && !constraint.empty?
224
+ def build_conda_requirements(constraint, file, channel = nil)
225
+ source = channel ? { channel: channel } : nil
213
226
 
214
227
  [{
215
- requirement: constraint,
228
+ requirement: constraint && !constraint.empty? ? constraint : nil,
216
229
  file: file.name,
217
- source: nil,
230
+ source: source,
218
231
  groups: ["dependencies"]
219
232
  }]
220
233
  end
@@ -223,7 +236,6 @@ module Dependabot
223
236
  params(dep_string: String, file: Dependabot::DependencyFile).returns(T.nilable(T::Hash[Symbol, T.untyped]))
224
237
  end
225
238
  def parse_pip_dependency_string(dep_string, file)
226
- # Handle pip-style specifications: requests==2.25.1, flask>=1.0.0
227
239
  match = dep_string.match(/^([a-zA-Z0-9_.-]+)(?:\s*(==|>=|>|<=|<|!=|~=)\s*([0-9][a-zA-Z0-9._+-]*))?$/)
228
240
  return nil unless match
229
241
 
@@ -231,22 +243,16 @@ module Dependabot
231
243
  operator = match[2]
232
244
  version = match[3]
233
245
 
234
- # Extract meaningful version information for security update purposes
235
246
  extracted_version = nil
236
247
  if version
237
248
  case operator
238
249
  when "==", "="
239
- # Exact version: use as-is
240
250
  extracted_version = version
241
251
  when ">=", "~="
242
- # Minimum version constraint: use the specified version as current
243
- # This allows security updates to work by treating the constraint as current version
244
252
  extracted_version = version
245
253
  when ">"
246
- # Greater than: we can't determine exact version, leave as nil
247
254
  extracted_version = nil
248
255
  when "<=", "<", "!="
249
- # Upper bounds or exclusions: not useful for determining current version
250
256
  extracted_version = nil
251
257
  end
252
258
  end
@@ -286,16 +292,12 @@ module Dependabot
286
292
  )
287
293
  end
288
294
 
289
- sig { params(package_name: String).returns(T::Boolean) }
290
- def python_package?(package_name)
291
- PythonPackageClassifier.python_package?(package_name)
292
- end
293
-
294
295
  sig { params(dep_string: String).returns(T::Boolean) }
295
296
  def fully_qualified_package?(dep_string)
296
- # Fully qualified packages have build strings after the version
297
- # Format: package=version=build_string
298
- # Example: python=3.9.7=h60c2a47_0_cpython
297
+ # Fully qualified: package=version=build_string (e.g., python=3.9.7=h60c2a47_0)
298
+ return false if dep_string.include?("==")
299
+ return false if dep_string.include?("[")
300
+
299
301
  dep_string.count("=") >= 2
300
302
  end
301
303