dependabot-uv 0.299.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +34 -0
  3. data/helpers/lib/__init__.py +0 -0
  4. data/helpers/lib/hasher.py +36 -0
  5. data/helpers/lib/parser.py +270 -0
  6. data/helpers/requirements.txt +13 -0
  7. data/helpers/run.py +22 -0
  8. data/lib/dependabot/uv/authed_url_builder.rb +31 -0
  9. data/lib/dependabot/uv/file_fetcher.rb +328 -0
  10. data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
  11. data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
  12. data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
  13. data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
  14. data/lib/dependabot/uv/file_parser.rb +437 -0
  15. data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
  16. data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
  17. data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
  18. data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
  19. data/lib/dependabot/uv/file_updater.rb +105 -0
  20. data/lib/dependabot/uv/language.rb +76 -0
  21. data/lib/dependabot/uv/language_version_manager.rb +114 -0
  22. data/lib/dependabot/uv/metadata_finder.rb +186 -0
  23. data/lib/dependabot/uv/name_normaliser.rb +26 -0
  24. data/lib/dependabot/uv/native_helpers.rb +38 -0
  25. data/lib/dependabot/uv/package_manager.rb +54 -0
  26. data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
  27. data/lib/dependabot/uv/pipenv_runner.rb +108 -0
  28. data/lib/dependabot/uv/requirement.rb +163 -0
  29. data/lib/dependabot/uv/requirement_parser.rb +60 -0
  30. data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
  31. data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
  32. data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
  33. data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
  34. data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
  35. data/lib/dependabot/uv/update_checker.rb +317 -0
  36. data/lib/dependabot/uv/version.rb +321 -0
  37. data/lib/dependabot/uv.rb +35 -0
  38. metadata +306 -0
@@ -0,0 +1,73 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/uv/requirement_parser"
5
+ require "dependabot/uv/file_updater"
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/uv/native_helpers"
8
+
9
+ module Dependabot
10
+ module Uv
11
+ class FileUpdater
12
+ class RequirementFileUpdater
13
+ require_relative "requirement_replacer"
14
+
15
+ attr_reader :dependencies
16
+ attr_reader :dependency_files
17
+ attr_reader :credentials
18
+
19
+ def initialize(dependencies:, dependency_files:, credentials:, index_urls: nil)
20
+ @dependencies = dependencies
21
+ @dependency_files = dependency_files
22
+ @credentials = credentials
23
+ @index_urls = index_urls
24
+ end
25
+
26
+ def updated_dependency_files
27
+ @updated_dependency_files ||= fetch_updated_dependency_files
28
+ end
29
+
30
+ private
31
+
32
+ def dependency
33
+ # For now, we'll only ever be updating a single dependency
34
+ dependencies.first
35
+ end
36
+
37
+ def fetch_updated_dependency_files
38
+ reqs = dependency.requirements.zip(dependency.previous_requirements)
39
+
40
+ reqs.filter_map do |(new_req, old_req)|
41
+ next if new_req == old_req
42
+
43
+ file = get_original_file(new_req.fetch(:file)).dup
44
+ updated_content =
45
+ updated_requirement_or_setup_file_content(new_req, old_req)
46
+ next if updated_content == file.content
47
+
48
+ file.content = updated_content
49
+ file
50
+ end
51
+ end
52
+
53
+ def updated_requirement_or_setup_file_content(new_req, old_req)
54
+ original_file = get_original_file(new_req.fetch(:file))
55
+ raise "Could not find a dependency file for #{new_req}" unless original_file
56
+
57
+ RequirementReplacer.new(
58
+ content: original_file.content,
59
+ dependency_name: dependency.name,
60
+ old_requirement: old_req.fetch(:requirement),
61
+ new_requirement: new_req.fetch(:requirement),
62
+ new_hash_version: dependency.version,
63
+ index_urls: @index_urls
64
+ ).updated_content
65
+ end
66
+
67
+ def get_original_file(filename)
68
+ dependency_files.find { |f| f.name == filename }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,214 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/dependency"
5
+ require "dependabot/uv/requirement_parser"
6
+ require "dependabot/uv/file_updater"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/uv/native_helpers"
9
+ require "dependabot/uv/name_normaliser"
10
+
11
+ module Dependabot
12
+ module Uv
13
+ class FileUpdater
14
+ class RequirementReplacer
15
+ PACKAGE_NOT_FOUND_ERROR = "PackageNotFoundError"
16
+
17
+ CERTIFICATE_VERIFY_FAILED = /CERTIFICATE_VERIFY_FAILED/
18
+
19
+ def initialize(content:, dependency_name:, old_requirement:,
20
+ new_requirement:, new_hash_version: nil, index_urls: nil)
21
+ @content = content
22
+ @dependency_name = normalise(dependency_name)
23
+ @old_requirement = old_requirement
24
+ @new_requirement = new_requirement
25
+ @new_hash_version = new_hash_version
26
+ @index_urls = index_urls
27
+ end
28
+
29
+ def updated_content
30
+ updated_content =
31
+ content.gsub(original_declaration_replacement_regex) do |mtch|
32
+ # If the "declaration" is setting an option (e.g., no-binary)
33
+ # ignore it, since it isn't actually a declaration
34
+ next mtch if Regexp.last_match&.pre_match&.match?(/--.*\z/)
35
+
36
+ updated_dependency_declaration_string
37
+ end
38
+
39
+ raise "Expected content to change!" if old_requirement != new_requirement && content == updated_content
40
+
41
+ updated_content
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :content
47
+ attr_reader :dependency_name
48
+ attr_reader :old_requirement
49
+ attr_reader :new_requirement
50
+ attr_reader :new_hash_version
51
+
52
+ def update_hashes?
53
+ !new_hash_version.nil?
54
+ end
55
+
56
+ def updated_requirement_string
57
+ new_req_string = new_requirement
58
+
59
+ new_req_string = new_req_string.gsub(/,\s*/, ", ") if add_space_after_commas?
60
+
61
+ if add_space_after_operators?
62
+ new_req_string =
63
+ new_req_string
64
+ .gsub(/(#{RequirementParser::COMPARISON})\s*(?=\d)/o, '\1 ')
65
+ end
66
+
67
+ new_req_string
68
+ end
69
+
70
+ def updated_dependency_declaration_string
71
+ old_req = old_requirement
72
+ updated_string =
73
+ if old_req
74
+ original_dependency_declaration_string(old_req)
75
+ .sub(RequirementParser::REQUIREMENTS, updated_requirement_string)
76
+ else
77
+ original_dependency_declaration_string(old_req)
78
+ .sub(RequirementParser::NAME_WITH_EXTRAS) do |nm|
79
+ nm + updated_requirement_string
80
+ end
81
+ end
82
+
83
+ return updated_string unless update_hashes? && requirement_includes_hashes?(old_req)
84
+
85
+ updated_string.sub(
86
+ RequirementParser::HASHES,
87
+ package_hashes_for(
88
+ name: dependency_name,
89
+ version: new_hash_version,
90
+ algorithm: hash_algorithm(old_req)
91
+ ).join(hash_separator(old_req))
92
+ )
93
+ end
94
+
95
+ def add_space_after_commas?
96
+ original_dependency_declaration_string(old_requirement)
97
+ .match(RequirementParser::REQUIREMENTS)
98
+ .to_s.include?(", ")
99
+ end
100
+
101
+ def add_space_after_operators?
102
+ original_dependency_declaration_string(old_requirement)
103
+ .match(RequirementParser::REQUIREMENTS)
104
+ .to_s.match?(/#{RequirementParser::COMPARISON}\s+\d/o)
105
+ end
106
+
107
+ def original_declaration_replacement_regex
108
+ original_string =
109
+ original_dependency_declaration_string(old_requirement)
110
+ /(?<![\-\w\.\[])#{Regexp.escape(original_string)}(?![\-\w\.])/
111
+ end
112
+
113
+ def requirement_includes_hashes?(requirement)
114
+ original_dependency_declaration_string(requirement)
115
+ .match?(RequirementParser::HASHES)
116
+ end
117
+
118
+ def hash_algorithm(requirement)
119
+ return unless requirement_includes_hashes?(requirement)
120
+
121
+ original_dependency_declaration_string(requirement)
122
+ .match(RequirementParser::HASHES)
123
+ .named_captures.fetch("algorithm")
124
+ end
125
+
126
+ def hash_separator(requirement)
127
+ return unless requirement_includes_hashes?(requirement)
128
+
129
+ hash_regex = RequirementParser::HASH
130
+ current_separator =
131
+ original_dependency_declaration_string(requirement)
132
+ .match(/#{hash_regex}((?<separator>\s*\\?\s*?)#{hash_regex})*/)
133
+ .named_captures.fetch("separator")
134
+
135
+ default_separator =
136
+ original_dependency_declaration_string(requirement)
137
+ .match(RequirementParser::HASH)
138
+ .pre_match.match(/(?<separator>\s*\\?\s*?)\z/)
139
+ .named_captures.fetch("separator")
140
+
141
+ current_separator || default_separator
142
+ end
143
+
144
+ def package_hashes_for(name:, version:, algorithm:)
145
+ index_urls = @index_urls || [nil]
146
+
147
+ index_urls.map do |index_url|
148
+ args = [name, version, algorithm]
149
+ args << index_url unless index_url.nil?
150
+
151
+ begin
152
+ result = SharedHelpers.run_helper_subprocess(
153
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
154
+ function: "get_dependency_hash",
155
+ args: args
156
+ )
157
+ rescue SharedHelpers::HelperSubprocessFailed => e
158
+ requirement_error_handler(e)
159
+
160
+ raise unless e.message.include?("PackageNotFoundError")
161
+
162
+ next
163
+ end
164
+
165
+ return result.map { |h| "--hash=#{algorithm}:#{h['hash']}" } if result.is_a?(Array)
166
+ end
167
+
168
+ raise Dependabot::DependencyFileNotResolvable, "Unable to find hashes for package #{name}"
169
+ end
170
+
171
+ def original_dependency_declaration_string(old_req)
172
+ matches = []
173
+
174
+ dec =
175
+ if old_req.nil?
176
+ regex = RequirementParser::INSTALL_REQ_WITHOUT_REQUIREMENT
177
+ content.scan(regex) { matches << Regexp.last_match }
178
+ matches.find { |m| normalise(m[:name]) == dependency_name }
179
+ else
180
+ regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
181
+ content.scan(regex) { matches << Regexp.last_match }
182
+ matches
183
+ .select { |m| normalise(m[:name]) == dependency_name }
184
+ .find { |m| requirements_match(m[:requirements], old_req) }
185
+ end
186
+
187
+ raise "Declaration not found for #{dependency_name}!" unless dec
188
+
189
+ dec.to_s.strip
190
+ end
191
+
192
+ def normalise(name)
193
+ NameNormaliser.normalise(name)
194
+ end
195
+
196
+ def requirements_match(req1, req2)
197
+ req1&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort ==
198
+ req2&.split(",")&.map { |r| r.gsub(/\s/, "") }&.sort
199
+ end
200
+
201
+ public
202
+
203
+ def requirement_error_handler(error)
204
+ Dependabot.logger.warn(error.message)
205
+
206
+ return unless error.message.match?(CERTIFICATE_VERIFY_FAILED)
207
+
208
+ msg = "Error resolving dependency."
209
+ raise DependencyFileNotResolvable, msg
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,105 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "toml-rb"
5
+ require "dependabot/file_updaters"
6
+ require "dependabot/file_updaters/base"
7
+ require "dependabot/shared_helpers"
8
+ require "sorbet-runtime"
9
+
10
+ module Dependabot
11
+ module Uv
12
+ class FileUpdater < Dependabot::FileUpdaters::Base
13
+ extend T::Sig
14
+
15
+ require_relative "file_updater/compile_file_updater"
16
+ require_relative "file_updater/requirement_file_updater"
17
+
18
+ sig { override.returns(T::Array[Regexp]) }
19
+ def self.updated_files_regex
20
+ [
21
+ /^.*\.txt$/, # Match any .txt files (e.g., requirements.txt) at any level
22
+ /^.*\.in$/, # Match any .in files at any level
23
+ /^.*pyproject\.toml$/ # Match pyproject.toml at any level
24
+ ]
25
+ end
26
+
27
+ sig { override.returns(T::Array[DependencyFile]) }
28
+ def updated_dependency_files
29
+ updated_files = updated_pip_compile_based_files
30
+
31
+ if updated_files.none? ||
32
+ updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
33
+ raise "No files have changed!"
34
+ end
35
+
36
+ updated_files
37
+ end
38
+
39
+ private
40
+
41
+ sig { returns(T.nilable(Symbol)) }
42
+ def subdependency_resolver
43
+ raise "Claimed to be a sub-dependency, but no lockfile exists!" if pip_compile_files.empty?
44
+
45
+ :pip_compile if pip_compile_files.any?
46
+ end
47
+
48
+ sig { returns(T::Array[DependencyFile]) }
49
+ def updated_pip_compile_based_files
50
+ CompileFileUpdater.new(
51
+ dependencies: dependencies,
52
+ dependency_files: dependency_files,
53
+ credentials: credentials,
54
+ index_urls: pip_compile_index_urls
55
+ ).updated_dependency_files
56
+ end
57
+
58
+ sig { returns(T::Array[DependencyFile]) }
59
+ def updated_requirement_based_files
60
+ RequirementFileUpdater.new(
61
+ dependencies: dependencies,
62
+ dependency_files: dependency_files,
63
+ credentials: credentials,
64
+ index_urls: pip_compile_index_urls
65
+ ).updated_dependency_files
66
+ end
67
+
68
+ sig { returns(T::Array[String]) }
69
+ def pip_compile_index_urls
70
+ if credentials.any?(&:replaces_base?)
71
+ credentials.select(&:replaces_base?).map { |cred| AuthedUrlBuilder.authed_url(credential: cred) }
72
+ else
73
+ urls = credentials.map { |cred| AuthedUrlBuilder.authed_url(credential: cred) }
74
+ # If there are no credentials that replace the base, we need to
75
+ # ensure that the base URL is included in the list of extra-index-urls.
76
+ [nil, *urls]
77
+ end
78
+ end
79
+
80
+ sig { override.void }
81
+ def check_required_files
82
+ filenames = dependency_files.map(&:name)
83
+ return if filenames.any? { |name| name.end_with?(".txt", ".in") }
84
+ return if pyproject
85
+
86
+ raise "Missing required files!"
87
+ end
88
+
89
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
90
+ def pyproject
91
+ @pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile))
92
+ end
93
+
94
+ sig { returns(T::Array[DependencyFile]) }
95
+ def pip_compile_files
96
+ @pip_compile_files ||= T.let(
97
+ dependency_files.select { |f| f.name.end_with?(".in") },
98
+ T.nilable(T::Array[DependencyFile])
99
+ )
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ Dependabot::FileUpdaters.register("uv", Dependabot::Uv::FileUpdater)
@@ -0,0 +1,76 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/uv/version"
6
+ require "dependabot/ecosystem"
7
+
8
+ module Dependabot
9
+ module Uv
10
+ LANGUAGE = "python"
11
+
12
+ class Language < Dependabot::Ecosystem::VersionManager
13
+ extend T::Sig
14
+ # These versions should match the versions specified at the top of `python/Dockerfile`
15
+ PYTHON_3_13 = "3.13"
16
+ PYTHON_3_12 = "3.12"
17
+ PYTHON_3_11 = "3.11"
18
+ PYTHON_3_10 = "3.10"
19
+ PYTHON_3_9 = "3.9"
20
+ PYTHON_3_8 = "3.8"
21
+
22
+ DEPRECATED_VERSIONS = T.let([Version.new(PYTHON_3_8)].freeze, T::Array[Dependabot::Version])
23
+
24
+ # Keep versions in ascending order
25
+ SUPPORTED_VERSIONS = T.let([
26
+ Version.new(PYTHON_3_9),
27
+ Version.new(PYTHON_3_10),
28
+ Version.new(PYTHON_3_11),
29
+ Version.new(PYTHON_3_12),
30
+ Version.new(PYTHON_3_13)
31
+ ].freeze, T::Array[Dependabot::Version])
32
+
33
+ sig do
34
+ params(
35
+ detected_version: String,
36
+ raw_version: T.nilable(String),
37
+ requirement: T.nilable(Requirement)
38
+ ).void
39
+ end
40
+ def initialize(detected_version:, raw_version: nil, requirement: nil)
41
+ super(
42
+ name: LANGUAGE,
43
+ detected_version: major_minor_version(detected_version),
44
+ version: raw_version ? Version.new(raw_version) : nil,
45
+ deprecated_versions: DEPRECATED_VERSIONS,
46
+ supported_versions: SUPPORTED_VERSIONS,
47
+ requirement: requirement,
48
+ )
49
+ end
50
+
51
+ sig { override.returns(T::Boolean) }
52
+ def deprecated?
53
+ return false unless detected_version
54
+ return false if unsupported?
55
+
56
+ deprecated_versions.include?(detected_version)
57
+ end
58
+
59
+ sig { override.returns(T::Boolean) }
60
+ def unsupported?
61
+ return false unless detected_version
62
+
63
+ supported_versions.all? { |supported| supported > detected_version }
64
+ end
65
+
66
+ private
67
+
68
+ sig { params(version: String).returns(Dependabot::Uv::Version) }
69
+ def major_minor_version(version)
70
+ major_minor = T.let(T.must(Version.new(version).segments[0..1]&.join(".")), String)
71
+
72
+ Version.new(major_minor)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,114 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/logger"
5
+ require "dependabot/uv/version"
6
+ require "sorbet-runtime"
7
+
8
+ module Dependabot
9
+ module Uv
10
+ class LanguageVersionManager
11
+ extend T::Sig
12
+ # This list must match the versions specified at the top of `python/Dockerfile`
13
+ PRE_INSTALLED_PYTHON_VERSIONS = %w(
14
+ 3.13.2
15
+ 3.12.9
16
+ 3.11.11
17
+ 3.10.16
18
+ 3.9.21
19
+ ).freeze
20
+
21
+ sig { params(python_requirement_parser: T.untyped).void }
22
+ def initialize(python_requirement_parser:)
23
+ @python_requirement_parser = python_requirement_parser
24
+ end
25
+
26
+ sig { returns(T.nilable(String)) }
27
+ def install_required_python
28
+ # The leading space is important in the version check
29
+ return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")
30
+
31
+ SharedHelpers.run_shell_command(
32
+ "tar -axf /usr/local/.pyenv/versions/#{python_version}.tar.zst -C /usr/local/.pyenv/versions"
33
+ )
34
+ end
35
+
36
+ sig { returns(String) }
37
+ def installed_version
38
+ # Use `pyenv exec` to query the active Python version
39
+ output, _status = SharedHelpers.run_shell_command("pyenv exec python --version")
40
+ version = output.strip.split.last # Extract the version number (e.g., "3.13.1")
41
+
42
+ T.must(version)
43
+ end
44
+
45
+ sig { returns(T.untyped) }
46
+ def python_major_minor
47
+ @python_major_minor ||= T.let(T.must(Uv::Version.new(python_version).segments[0..1]).join("."), T.untyped)
48
+ end
49
+
50
+ sig { returns(String) }
51
+ def python_version
52
+ @python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
53
+ end
54
+
55
+ sig { returns(String) }
56
+ def python_requirement_string
57
+ if user_specified_python_version
58
+ if user_specified_python_version.start_with?(/\d/)
59
+ parts = user_specified_python_version.split(".")
60
+ parts.fill("*", (parts.length)..2).join(".")
61
+ else
62
+ user_specified_python_version
63
+ end
64
+ else
65
+ python_version_matching_imputed_requirements || PRE_INSTALLED_PYTHON_VERSIONS.first
66
+ end
67
+ end
68
+
69
+ sig { returns(String) }
70
+ def python_version_from_supported_versions
71
+ requirement_string = python_requirement_string
72
+
73
+ # If the requirement string isn't already a range (eg ">3.10"), coerce it to "major.minor.*".
74
+ # The patch version is ignored because a non-matching patch version is unlikely to affect resolution.
75
+ requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if requirement_string.start_with?(/\d/)
76
+
77
+ # Try to match one of our pre-installed Python versions
78
+ requirement = T.must(Uv::Requirement.requirements_array(requirement_string).first)
79
+ version = PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(Uv::Version.new(v)) }
80
+ return version if version
81
+
82
+ # Otherwise we have to raise
83
+ supported_versions = PRE_INSTALLED_PYTHON_VERSIONS.map { |x| x.gsub(/\.\d+$/, ".*") }.join(", ")
84
+ raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
85
+ end
86
+
87
+ sig { returns(T.untyped) }
88
+ def user_specified_python_version
89
+ @python_requirement_parser.user_specified_requirements.first
90
+ end
91
+
92
+ sig { returns(T.nilable(String)) }
93
+ def python_version_matching_imputed_requirements
94
+ compiled_file_python_requirement_markers =
95
+ @python_requirement_parser.imputed_requirements.map do |r|
96
+ Dependabot::Uv::Requirement.new(r)
97
+ end
98
+ python_version_matching(compiled_file_python_requirement_markers)
99
+ end
100
+
101
+ sig { params(requirements: T.untyped).returns(T.nilable(String)) }
102
+ def python_version_matching(requirements)
103
+ PRE_INSTALLED_PYTHON_VERSIONS.find do |version_string|
104
+ version = Uv::Version.new(version_string)
105
+ requirements.all? do |req|
106
+ next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
107
+
108
+ req.satisfied_by?(version)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end