dependabot-conda 0.325.1

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.
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/metadata_finders"
6
+ require "dependabot/metadata_finders/base"
7
+ require "dependabot/conda/python_package_classifier"
8
+ require "dependabot/python/metadata_finder"
9
+
10
+ module Dependabot
11
+ module Conda
12
+ class MetadataFinder < Dependabot::MetadataFinders::Base
13
+ extend T::Sig
14
+
15
+ sig { override.returns(T.nilable(String)) }
16
+ def homepage_url
17
+ return super unless python_package?(dependency.name)
18
+
19
+ # Delegate to Python metadata finder for enhanced PyPI-based homepage URLs
20
+ python_metadata_finder.homepage_url
21
+ end
22
+
23
+ private
24
+
25
+ sig { override.returns(T.nilable(Dependabot::Source)) }
26
+ def look_up_source
27
+ return nil unless python_package?(dependency.name)
28
+
29
+ # Delegate to Python metadata finder for Python packages
30
+ python_metadata_finder.send(:look_up_source)
31
+ end
32
+
33
+ sig { params(package_name: String).returns(T::Boolean) }
34
+ def python_package?(package_name)
35
+ PythonPackageClassifier.python_package?(package_name)
36
+ end
37
+
38
+ sig { returns(Dependabot::Python::MetadataFinder) }
39
+ 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
+ @python_metadata_finder ||= T.let(
44
+ Dependabot::Python::MetadataFinder.new(
45
+ dependency: python_dependency,
46
+ credentials: credentials
47
+ ),
48
+ T.nilable(Dependabot::Python::MetadataFinder)
49
+ )
50
+ @python_metadata_finder
51
+ end
52
+
53
+ sig { returns(Dependabot::Dependency) }
54
+ def python_dependency
55
+ Dependabot::Dependency.new(
56
+ name: dependency.name,
57
+ version: dependency.version,
58
+ requirements: dependency.requirements.map do |req|
59
+ req.merge(
60
+ file: req[:file]&.gsub(/environment\.ya?ml/, "requirements.txt"),
61
+ source: nil # No private pip-index in conda manifests
62
+ )
63
+ end,
64
+ package_manager: "pip"
65
+ )
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ Dependabot::MetadataFinders.register("conda", Dependabot::Conda::MetadataFinder)
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Conda
8
+ class NameNormaliser
9
+ extend T::Sig
10
+
11
+ sig { params(name: String).returns(String) }
12
+ def self.normalise(name)
13
+ # Conda package names follow similar rules to Python packages
14
+ # Convert to lowercase and replace underscores/dots with hyphens
15
+ name.downcase.tr("_.", "-")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/ecosystem"
6
+ require "dependabot/conda/version"
7
+
8
+ module Dependabot
9
+ module Conda
10
+ ECOSYSTEM = "conda"
11
+
12
+ SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
13
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
14
+
15
+ class CondaPackageManager < Dependabot::Ecosystem::VersionManager
16
+ extend T::Sig
17
+
18
+ NAME = "conda"
19
+ VERSION = "latest"
20
+
21
+ SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
22
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
23
+
24
+ sig { void }
25
+ def initialize
26
+ super(
27
+ name: NAME,
28
+ version: Dependabot::Conda::Version.new(VERSION),
29
+ deprecated_versions: DEPRECATED_VERSIONS,
30
+ supported_versions: SUPPORTED_VERSIONS
31
+ )
32
+ end
33
+
34
+ sig { override.returns(T::Boolean) }
35
+ def deprecated?
36
+ false
37
+ end
38
+
39
+ sig { override.returns(T::Boolean) }
40
+ def unsupported?
41
+ false
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,85 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Conda
8
+ class PythonPackageClassifier
9
+ extend T::Sig
10
+
11
+ # Known non-Python packages that should be ignored
12
+ NON_PYTHON_PATTERNS = T.let([
13
+ /^r-/i, # R packages (r-base, r-essentials, etc.)
14
+ /^r$/i, # R language itself
15
+ /^python$/i, # Python interpreter (conda-specific, not on PyPI)
16
+ /^git$/i, # Git version control
17
+ /^gcc$/i, # GCC compiler
18
+ /^cmake$/i, # CMake build system
19
+ /^make$/i, # Make build tool
20
+ /^curl$/i, # cURL utility
21
+ /^wget$/i, # Wget utility
22
+ /^vim$/i, # Vim editor
23
+ /^nano$/i, # Nano editor
24
+ /^nodejs$/i, # Node.js runtime
25
+ /^java$/i, # Java runtime
26
+ /^go$/i, # Go language
27
+ /^rust$/i, # Rust language
28
+ /^julia$/i, # Julia language
29
+ /^perl$/i, # Perl language
30
+ /^ruby$/i, # Ruby language
31
+ # System libraries
32
+ /^openssl$/i, # OpenSSL
33
+ /^zlib$/i, # zlib compression
34
+ /^libffi$/i, # Foreign Function Interface library
35
+ /^ncurses$/i, # Terminal control library
36
+ /^readline$/i, # Command line editing
37
+ # Compiler and build tools
38
+ /^_libgcc_mutex$/i,
39
+ /^_openmp_mutex$/i,
40
+ /^binutils$/i,
41
+ /^gxx_linux-64$/i,
42
+ # Multimedia libraries
43
+ /^ffmpeg$/i, # Video processing
44
+ /^opencv$/i, # Computer vision (note: opencv-python is different)
45
+ /^imageio$/i # Image I/O (note: imageio python package is different)
46
+ ].freeze, T::Array[Regexp])
47
+
48
+ # Determine if a package name represents a Python package
49
+ sig { params(package_name: String).returns(T::Boolean) }
50
+ def self.python_package?(package_name)
51
+ return false if package_name.empty?
52
+
53
+ # Extract just the package name without version or channel information
54
+ normalized_name = extract_package_name(package_name).downcase.strip
55
+ return false if normalized_name.empty?
56
+
57
+ # Check if it's explicitly a non-Python package
58
+ return false if NON_PYTHON_PATTERNS.any? { |pattern| normalized_name.match?(pattern) }
59
+
60
+ # Block obvious binary/system files
61
+ return false if normalized_name.match?(/\.(exe|dll|so|dylib)$/i)
62
+ return false if normalized_name.match?(/^lib.+\.a$/i) # Static libraries
63
+
64
+ # Block system mutexes
65
+ return false if normalized_name.match?(/^_[a-z0-9]+_mutex$/i)
66
+
67
+ # Default: treat as Python package
68
+ # This aligns with the strategic decision to focus on Python packages
69
+ # Most packages in conda environments are Python packages
70
+ true
71
+ end
72
+
73
+ # Extract package name from conda specification (remove channel prefix if present)
74
+ sig { params(spec: String).returns(String) }
75
+ def self.extract_package_name(spec)
76
+ # Handle channel specifications like "conda-forge::numpy=1.21.0"
77
+ parts = spec.split("::")
78
+ package_spec = parts.last || spec
79
+
80
+ # Extract package name (before = or space or version operators)
81
+ package_spec.split(/[=<>!~\s]/).first&.strip || spec
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,133 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/requirement"
6
+ require "dependabot/python/version"
7
+ require "dependabot/conda/version"
8
+
9
+ module Dependabot
10
+ module Conda
11
+ class Requirement < Dependabot::Requirement
12
+ extend T::Sig
13
+
14
+ # Conda uses different operators than pip:
15
+ # conda: =, >=, >, <, <=, !=, ~
16
+ # pip: ==, >=, >, <, <=, !=, ~=
17
+
18
+ # Support both conda and pip operators
19
+ OPS = T.let(OPS.merge(
20
+ "=" => ->(v, r) { v == r }, # conda equality
21
+ "==" => ->(v, r) { v == r }, # pip equality
22
+ "~=" => ->(v, r) { v >= r && v.release < r.bump } # pip compatible release
23
+ ), T::Hash[String, T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)])
24
+
25
+ quoted = OPS.keys.sort_by(&:length).reverse
26
+ .map { |k| Regexp.quote(k) }.join("|")
27
+ # Use Python version pattern since conda version inherits from it
28
+ version_pattern = Dependabot::Python::Version::VERSION_PATTERN
29
+
30
+ PATTERN_RAW = T.let("\\s*(?<op>#{quoted})?\\s*(?<version>#{version_pattern})\\s*".freeze, String)
31
+ PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp)
32
+
33
+ sig { params(obj: T.any(Gem::Version, String)).returns([String, Gem::Version]) }
34
+ def self.parse(obj)
35
+ return ["=", obj] if obj.is_a?(Gem::Version)
36
+
37
+ unless (matches = PATTERN.match(obj.to_s))
38
+ msg = "Illformed requirement [#{obj.inspect}]"
39
+ raise BadRequirementError, msg
40
+ end
41
+
42
+ return DefaultRequirement if matches[:op] == ">=" && matches[:version] == "0"
43
+
44
+ [matches[:op] || "=", Dependabot::Conda::Version.new(T.must(matches[:version]))]
45
+ end
46
+
47
+ sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
48
+ def self.requirements_array(requirement_string)
49
+ return [new(nil)] if requirement_string.nil?
50
+
51
+ # Handle complex requirements like ">=1.0.0,<2.0.0"
52
+ requirement_string.strip.split(",").map do |req_string|
53
+ new(req_string.strip)
54
+ end
55
+ end
56
+
57
+ sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void }
58
+ def initialize(*requirements)
59
+ @original_string = T.let(requirements.first&.to_s, T.nilable(String))
60
+
61
+ requirements = requirements.flatten.flat_map do |req_string|
62
+ next if req_string.nil?
63
+
64
+ # Handle complex requirements and convert to Ruby-compatible format
65
+ req_string.split(",").map(&:strip).map do |r|
66
+ convert_conda_constraint_to_ruby_constraint(r)
67
+ end
68
+ end.compact
69
+
70
+ super(requirements)
71
+ end
72
+
73
+ sig { params(version: T.any(Gem::Version, String)).returns(T::Boolean) }
74
+ def satisfied_by?(version)
75
+ version = Dependabot::Conda::Version.new(version.to_s)
76
+
77
+ requirements.all? { |op, rv| T.must(OPS[op] || OPS["="]).call(version, rv) }
78
+ end
79
+
80
+ sig { returns(T::Boolean) }
81
+ def exact?
82
+ return false unless requirements.size == 1
83
+
84
+ %w(= == ===).include?(requirements[0][0])
85
+ end
86
+
87
+ sig { returns(String) }
88
+ def to_s
89
+ @original_string || super
90
+ end
91
+
92
+ private
93
+
94
+ sig { params(req_string: String).returns(T.nilable(T.any(String, T::Array[String]))) }
95
+ def convert_conda_constraint_to_ruby_constraint(req_string)
96
+ return nil if req_string.strip.empty?
97
+ return nil if req_string == "*"
98
+
99
+ # Handle conda wildcard patterns like "=1.2.*"
100
+ return convert_wildcard_requirement(req_string) if req_string.match?(/=\s*\d+(\.\d+)*\.\*/)
101
+
102
+ # Handle pip-style compatible release operator
103
+ req_string = req_string.gsub("~=", "~>") if req_string.include?("~=")
104
+
105
+ # Convert conda single = to Ruby = for internal processing
106
+ req_string
107
+ end
108
+
109
+ sig { params(req_string: String).returns(T.any(String, T::Array[String])) }
110
+ def convert_wildcard_requirement(req_string)
111
+ # Convert "=1.2.*" to appropriate range constraints
112
+ version_part = req_string.gsub(/^=\s*/, "").gsub(/\.\*$/, "")
113
+ parts = version_part.split(".")
114
+
115
+ if parts.length == 1
116
+ # "=1.*" becomes ">= 1.0.0, < 2.0.0"
117
+ major = parts[0].to_i
118
+ [">= #{major}.0.0", "< #{major + 1}.0.0"]
119
+ elsif parts.length == 2
120
+ # "=1.2.*" becomes ">= 1.2.0, < 1.3.0"
121
+ major = parts[0].to_i
122
+ minor = parts[1].to_i
123
+ [">= #{major}.#{minor}.0", "< #{major}.#{minor + 1}.0"]
124
+ else
125
+ # Fallback to exact match without wildcard
126
+ "= #{version_part}.0"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ Dependabot::Utils.register_requirement_class("conda", Dependabot::Conda::Requirement)
@@ -0,0 +1,91 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/package/package_latest_version_finder"
6
+ require "dependabot/python/update_checker/latest_version_finder"
7
+ require "dependabot/dependency"
8
+ require_relative "requirement_translator"
9
+
10
+ module Dependabot
11
+ module Conda
12
+ class UpdateChecker
13
+ class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
14
+ extend T::Sig
15
+
16
+ sig do
17
+ params(
18
+ dependency: Dependabot::Dependency,
19
+ dependency_files: T::Array[Dependabot::DependencyFile],
20
+ credentials: T::Array[Dependabot::Credential],
21
+ ignored_versions: T::Array[String],
22
+ raise_on_ignored: T::Boolean,
23
+ security_advisories: T::Array[Dependabot::SecurityAdvisory],
24
+ cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
25
+ ).void
26
+ end
27
+ def initialize(dependency:, dependency_files:, credentials:,
28
+ ignored_versions:, raise_on_ignored:, security_advisories:,
29
+ cooldown_options:)
30
+ @raise_on_ignored = T.let(raise_on_ignored, T::Boolean)
31
+ @cooldown_options = T.let(cooldown_options, T.nilable(Dependabot::Package::ReleaseCooldownOptions))
32
+
33
+ super
34
+ end
35
+
36
+ sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) }
37
+ def package_details
38
+ @package_details ||= python_latest_version_finder.package_details
39
+ end
40
+
41
+ sig { override.returns(T::Boolean) }
42
+ def cooldown_enabled?
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ sig { returns(Dependabot::Python::UpdateChecker::LatestVersionFinder) }
49
+ def python_latest_version_finder
50
+ @python_latest_version_finder ||= T.let(
51
+ Dependabot::Python::UpdateChecker::LatestVersionFinder.new(
52
+ dependency: python_compatible_dependency,
53
+ dependency_files: dependency_files,
54
+ credentials: credentials,
55
+ ignored_versions: ignored_versions,
56
+ raise_on_ignored: @raise_on_ignored,
57
+ security_advisories: security_advisories,
58
+ cooldown_options: @cooldown_options
59
+ ),
60
+ T.nilable(Dependabot::Python::UpdateChecker::LatestVersionFinder)
61
+ )
62
+ end
63
+
64
+ sig { returns(Dependabot::Dependency) }
65
+ def python_compatible_dependency
66
+ # Convert conda dependency to python-compatible dependency
67
+ Dependabot::Dependency.new(
68
+ name: dependency.name,
69
+ version: dependency.version,
70
+ requirements: python_compatible_requirements,
71
+ package_manager: "pip" # Use pip for PyPI compatibility
72
+ )
73
+ end
74
+
75
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
76
+ def python_compatible_requirements
77
+ dependency.requirements.map do |req|
78
+ req.merge(
79
+ requirement: convert_conda_requirement_to_pip(req[:requirement])
80
+ )
81
+ end
82
+ end
83
+
84
+ sig { params(conda_requirement: T.nilable(String)).returns(T.nilable(String)) }
85
+ def convert_conda_requirement_to_pip(conda_requirement)
86
+ RequirementTranslator.conda_to_pip(conda_requirement)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,57 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Conda
8
+ class UpdateChecker
9
+ class RequirementTranslator
10
+ extend T::Sig
11
+
12
+ # Convert conda-style requirements to pip-compatible requirements
13
+ sig { params(conda_requirement: T.nilable(String)).returns(T.nilable(String)) }
14
+ def self.conda_to_pip(conda_requirement)
15
+ return nil unless conda_requirement
16
+
17
+ # Handle different conda requirement patterns
18
+ case conda_requirement
19
+ when /^=([0-9]+(?:\.[0-9]+)*)\.\*$/
20
+ # Handle wildcards: =1.21.* -> >=1.21.0,<1.22.0
21
+ convert_wildcard_requirement(conda_requirement)
22
+ when /^=([0-9]+(?:\.[0-9]+)*)$/
23
+ # Handle exact equality: =1.2.3 -> ==1.2.3
24
+ conda_requirement.gsub(/^=/, "==")
25
+ when /^(>=|>|<=|<|!=)(.+)$/
26
+ # Handle comparison operators: >=1.2.0 -> >=1.2.0 (no change)
27
+ conda_requirement
28
+ when /^([0-9]+(?:\.[0-9]+)*)$/
29
+ # Handle bare version: 1.2.3 -> ==1.2.3
30
+ "==#{conda_requirement}"
31
+ else
32
+ # Handle complex constraints: >=3.8,<3.11 -> >=3.8,<3.11 (no change)
33
+ conda_requirement
34
+ end
35
+ end
36
+
37
+ sig { params(wildcard_requirement: String).returns(String) }
38
+ def self.convert_wildcard_requirement(wildcard_requirement)
39
+ # Extract version pattern: =1.21.* -> 1.21
40
+ version_match = wildcard_requirement.match(/^=([0-9]+(?:\.[0-9]+)*)\.\*$/)
41
+ return wildcard_requirement unless version_match
42
+
43
+ base_version = version_match[1]
44
+ version_parts = T.must(base_version).split(".")
45
+
46
+ # Calculate next version for upper bound
47
+ next_version_parts = version_parts.dup
48
+ next_version_parts[-1] = (next_version_parts[-1].to_i + 1).to_s
49
+ next_version = next_version_parts.join(".")
50
+
51
+ # Return range: >=1.21.0,<1.22.0
52
+ ">=#{base_version}.0,<#{next_version}.0"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end