dependabot-uv 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.
- checksums.yaml +4 -4
- data/helpers/requirements.txt +1 -1
- data/lib/dependabot/uv/authed_url_builder.rb +3 -27
- data/lib/dependabot/uv/file_updater/lock_file_updater.rb +38 -27
- data/lib/dependabot/uv/language.rb +6 -76
- data/lib/dependabot/uv/language_version_manager.rb +6 -131
- data/lib/dependabot/uv/metadata_finder.rb +6 -210
- data/lib/dependabot/uv/name_normaliser.rb +3 -17
- data/lib/dependabot/uv/native_helpers.rb +4 -30
- data/lib/dependabot/uv/package.rb +27 -0
- data/lib/dependabot/uv/requirement.rb +4 -196
- data/lib/dependabot/uv/requirement_parser.rb +5 -53
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +10 -16
- data/lib/dependabot/uv/version.rb +4 -321
- metadata +19 -8
- data/lib/dependabot/uv/file_parser/setup_file_parser.rb +0 -194
- data/lib/dependabot/uv/package/package_details_fetcher.rb +0 -486
- data/lib/dependabot/uv/package/package_registry_finder.rb +0 -288
- data/lib/dependabot/uv/pipenv_runner.rb +0 -110
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f74cec4813c7f6b11b51b6f73a10db68327ca41a2737e469ba5d9ea3b4612bea
|
|
4
|
+
data.tar.gz: e17b701d94aeafdca486c2303d23ea3b423eebbddd93d347ce1224160920cab5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32c427d81b22edb5d8b145ced9080a7fca9264e371da8f4a9ff5ca57c89b58f69179b4ade15620e24bb6a5d43194d959489d0c10942b050be8fb5e0c13327121
|
|
7
|
+
data.tar.gz: 615a296656b8957f897e5b17e3f951b143579bbc08aa8e8c806928bcd3e3349e4f0e3a54f0dcf3b5449fa7c8b6a892677e9a865925ec1fb044094ee35f4de527
|
data/helpers/requirements.txt
CHANGED
|
@@ -2,35 +2,11 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/python/authed_url_builder"
|
|
5
6
|
|
|
6
7
|
module Dependabot
|
|
7
8
|
module Uv
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sig { params(credential: Credential).returns(String) }
|
|
12
|
-
def self.authed_url(credential:)
|
|
13
|
-
token = T.let(credential.fetch("token", nil), T.nilable(String))
|
|
14
|
-
url = T.let(credential.fetch("index-url", nil), T.nilable(String))
|
|
15
|
-
return "" unless url
|
|
16
|
-
return url unless token
|
|
17
|
-
|
|
18
|
-
basic_auth_details =
|
|
19
|
-
if token.ascii_only? && token.include?(":") then token
|
|
20
|
-
elsif Base64.decode64(token).ascii_only? &&
|
|
21
|
-
Base64.decode64(token).include?(":")
|
|
22
|
-
Base64.decode64(token)
|
|
23
|
-
else
|
|
24
|
-
token
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
if basic_auth_details.include?(":")
|
|
28
|
-
username, _, password = basic_auth_details.partition(":")
|
|
29
|
-
basic_auth_details = "#{CGI.escape(username)}:#{CGI.escape(password)}"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
url.sub("://", "://#{basic_auth_details}@")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
9
|
+
# UV uses the same authenticated URL building logic as Python
|
|
10
|
+
AuthedUrlBuilder = Dependabot::Python::AuthedUrlBuilder
|
|
35
11
|
end
|
|
36
12
|
end
|
|
@@ -127,16 +127,39 @@ module Dependabot
|
|
|
127
127
|
def replace_dep(dep, content, new_r, old_r)
|
|
128
128
|
new_req = new_r[:requirement]
|
|
129
129
|
old_req = old_r[:requirement]
|
|
130
|
+
escaped_name = Regexp.escape(dep.name)
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
regex = /(["']#{escaped_name})([^"']+)(["'])/x
|
|
133
|
+
|
|
134
|
+
replaced = T.let(false, T::Boolean)
|
|
135
|
+
|
|
136
|
+
updated_content = content.gsub(regex) do
|
|
137
|
+
captured_requirement = Regexp.last_match(2)
|
|
138
|
+
|
|
139
|
+
if requirements_match?(T.must(captured_requirement), old_req)
|
|
140
|
+
replaced = true
|
|
141
|
+
"#{Regexp.last_match(1)}#{new_req}#{Regexp.last_match(3)}"
|
|
142
|
+
else
|
|
143
|
+
Regexp.last_match(0)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
unless replaced
|
|
148
|
+
updated_content = content.sub(regex) do
|
|
149
|
+
"#{Regexp.last_match(1)}#{new_req}#{Regexp.last_match(3)}"
|
|
150
|
+
end
|
|
139
151
|
end
|
|
152
|
+
|
|
153
|
+
updated_content
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
sig { params(req1: String, req2: String).returns(T::Boolean) }
|
|
157
|
+
def requirements_match?(req1, req2)
|
|
158
|
+
normalize = lambda do |req|
|
|
159
|
+
req.split(",").map(&:strip).sort.join(",")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
normalize.call(req1) == normalize.call(req2)
|
|
140
163
|
end
|
|
141
164
|
|
|
142
165
|
sig { returns(String) }
|
|
@@ -262,7 +285,13 @@ module Dependabot
|
|
|
262
285
|
options_fingerprint = lock_options_fingerprint(options)
|
|
263
286
|
|
|
264
287
|
# Use pyenv exec to ensure we're using the correct Python environment
|
|
265
|
-
|
|
288
|
+
# Include the target version to respect ignore conditions and avoid upgrading
|
|
289
|
+
# to the absolute latest version (which may be blocked by ignore rules)
|
|
290
|
+
dep_name = T.must(dependency).name
|
|
291
|
+
dep_version = T.must(dependency).version
|
|
292
|
+
package_spec = dep_version ? "#{dep_name}==#{dep_version}" : dep_name
|
|
293
|
+
|
|
294
|
+
command = "pyenv exec uv lock --upgrade-package #{package_spec} #{options}"
|
|
266
295
|
fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
|
|
267
296
|
|
|
268
297
|
run_command(command, fingerprint:)
|
|
@@ -313,24 +342,6 @@ module Dependabot
|
|
|
313
342
|
url.gsub(%r{^https?://}, "").gsub(/[^a-zA-Z0-9]/, "_").upcase
|
|
314
343
|
end
|
|
315
344
|
|
|
316
|
-
sig { params(dep: T.untyped, old_req: T.untyped).returns(Regexp) }
|
|
317
|
-
def declaration_regex(dep, old_req)
|
|
318
|
-
escaped_name = Regexp.escape(dep.name)
|
|
319
|
-
# Extract the requirement operator and version
|
|
320
|
-
operator = old_req.fetch(:requirement).match(/^(.+?)[0-9]/)&.captures&.first
|
|
321
|
-
# Escape special regex characters in the operator
|
|
322
|
-
escaped_operator = Regexp.escape(operator) if operator
|
|
323
|
-
|
|
324
|
-
# Match various formats of dependency declarations:
|
|
325
|
-
# 1. "dependency==1.0.0" (with quotes around the entire string)
|
|
326
|
-
# 2. dependency==1.0.0 (without quotes)
|
|
327
|
-
# The declaration should only include the package name, operator, and version
|
|
328
|
-
# without the enclosing quotes
|
|
329
|
-
/
|
|
330
|
-
["']?(?<declaration>#{escaped_name}\s*#{escaped_operator}[\d\.\*]+)["']?
|
|
331
|
-
/x
|
|
332
|
-
end
|
|
333
|
-
|
|
334
345
|
sig { returns(String) }
|
|
335
346
|
def lock_options
|
|
336
347
|
options = lock_index_options
|
|
@@ -1,84 +1,14 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
5
|
-
require "dependabot/uv/version"
|
|
6
|
-
require "dependabot/ecosystem"
|
|
4
|
+
require "dependabot/python/language"
|
|
7
5
|
|
|
8
6
|
module Dependabot
|
|
9
7
|
module Uv
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# This list must match the versions specified at the top of `uv/Dockerfile`
|
|
16
|
-
# ARG PY_3_13=3.13.2
|
|
17
|
-
# When updating this list, also update python/lib/dependabot/python/language.rb
|
|
18
|
-
PRE_INSTALLED_PYTHON_VERSIONS_RAW = %w(
|
|
19
|
-
3.14.0
|
|
20
|
-
3.13.9
|
|
21
|
-
3.12.12
|
|
22
|
-
3.11.14
|
|
23
|
-
3.10.19
|
|
24
|
-
3.9.24
|
|
25
|
-
).freeze
|
|
26
|
-
|
|
27
|
-
PRE_INSTALLED_PYTHON_VERSIONS = T.let(
|
|
28
|
-
PRE_INSTALLED_PYTHON_VERSIONS_RAW.map do |v|
|
|
29
|
-
Version.new(v)
|
|
30
|
-
end.sort,
|
|
31
|
-
T::Array[Version]
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
PRE_INSTALLED_VERSIONS_MAP = T.let(
|
|
35
|
-
PRE_INSTALLED_PYTHON_VERSIONS.to_h do |v|
|
|
36
|
-
[Version.new(T.must(v.segments[0..1]).join(".")), v]
|
|
37
|
-
end,
|
|
38
|
-
T::Hash[Version, Version]
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
PRE_INSTALLED_HIGHEST_VERSION = T.let(T.must(PRE_INSTALLED_PYTHON_VERSIONS.max), Version)
|
|
42
|
-
|
|
43
|
-
SUPPORTED_VERSIONS = T.let(
|
|
44
|
-
PRE_INSTALLED_PYTHON_VERSIONS.map do |v|
|
|
45
|
-
Version.new(T.must(v.segments[0..1]&.join(".")))
|
|
46
|
-
end,
|
|
47
|
-
T::Array[Version]
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
NON_SUPPORTED_HIGHEST_VERSION = "3.8"
|
|
51
|
-
|
|
52
|
-
DEPRECATED_VERSIONS = T.let([Version.new(NON_SUPPORTED_HIGHEST_VERSION)].freeze, T::Array[Dependabot::Version])
|
|
53
|
-
|
|
54
|
-
sig do
|
|
55
|
-
params(
|
|
56
|
-
detected_version: T.nilable(String),
|
|
57
|
-
raw_version: T.nilable(String),
|
|
58
|
-
requirement: T.nilable(Requirement)
|
|
59
|
-
).void
|
|
60
|
-
end
|
|
61
|
-
def initialize(detected_version:, raw_version: nil, requirement: nil)
|
|
62
|
-
super(
|
|
63
|
-
name: LANGUAGE,
|
|
64
|
-
detected_version: detected_version ? major_minor_version(detected_version) : nil,
|
|
65
|
-
version: raw_version ? Version.new(raw_version) : nil,
|
|
66
|
-
deprecated_versions: DEPRECATED_VERSIONS,
|
|
67
|
-
supported_versions: SUPPORTED_VERSIONS,
|
|
68
|
-
requirement: requirement,
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
sig { params(version: String).returns(T.nilable(Version)) }
|
|
75
|
-
def major_minor_version(version)
|
|
76
|
-
return nil if version.empty?
|
|
77
|
-
|
|
78
|
-
major_minor = T.let(T.must(Version.new(version).segments[0..1]&.join(".")), String)
|
|
79
|
-
|
|
80
|
-
Version.new(major_minor)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
8
|
+
# Both uv and Python ecosystems use the same Python language versions.
|
|
9
|
+
# The Python version list is maintained in python/lib/dependabot/python/language.rb
|
|
10
|
+
# and shared via this alias to avoid dual-maintenance.
|
|
11
|
+
LANGUAGE = Python::LANGUAGE
|
|
12
|
+
Language = Python::Language
|
|
83
13
|
end
|
|
84
14
|
end
|
|
@@ -1,138 +1,13 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "dependabot/
|
|
5
|
-
require "dependabot/uv/version"
|
|
6
|
-
require "sorbet-runtime"
|
|
4
|
+
require "dependabot/python/language_version_manager"
|
|
7
5
|
|
|
8
6
|
module Dependabot
|
|
9
7
|
module Uv
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def initialize(python_requirement_parser:)
|
|
15
|
-
@python_requirement_parser = python_requirement_parser
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
sig { returns(T.nilable(String)) }
|
|
19
|
-
def install_required_python
|
|
20
|
-
# The leading space is important in the version check
|
|
21
|
-
return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")
|
|
22
|
-
|
|
23
|
-
SharedHelpers.run_shell_command(
|
|
24
|
-
"tar -axf /usr/local/.pyenv/versions/#{python_version}.tar.zst -C /usr/local/.pyenv/versions"
|
|
25
|
-
)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
sig { returns(String) }
|
|
29
|
-
def installed_version
|
|
30
|
-
# Use `pyenv exec` to query the active Python version
|
|
31
|
-
output, _status = SharedHelpers.run_shell_command("pyenv exec python --version")
|
|
32
|
-
version = output.strip.split.last # Extract the version number (e.g., "3.13.1")
|
|
33
|
-
|
|
34
|
-
T.must(version)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
sig { returns(T.untyped) }
|
|
38
|
-
def python_major_minor
|
|
39
|
-
@python_major_minor ||= T.let(T.must(Version.new(python_version).segments[0..1]).join("."), T.untyped)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
sig { returns(String) }
|
|
43
|
-
def python_version
|
|
44
|
-
@python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
sig { returns(String) }
|
|
48
|
-
def python_requirement_string
|
|
49
|
-
if user_specified_python_version
|
|
50
|
-
if user_specified_python_version.start_with?(/\d/)
|
|
51
|
-
parts = user_specified_python_version.split(".")
|
|
52
|
-
parts.fill("*", (parts.length)..2).join(".")
|
|
53
|
-
else
|
|
54
|
-
user_specified_python_version
|
|
55
|
-
end
|
|
56
|
-
else
|
|
57
|
-
python_version_matching_imputed_requirements || Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
sig { params(requirement_string: T.nilable(String)).returns(T.nilable(String)) }
|
|
62
|
-
def normalize_python_exact_version(requirement_string)
|
|
63
|
-
return requirement_string if requirement_string.nil? || requirement_string.strip.empty?
|
|
64
|
-
|
|
65
|
-
requirement_string = requirement_string.strip
|
|
66
|
-
|
|
67
|
-
# If the requirement already has a wildcard, return nil
|
|
68
|
-
return nil if requirement_string == "*"
|
|
69
|
-
|
|
70
|
-
# If the requirement is not an exact version such as not X.Y.Z, =X.Y.Z, ==X.Y.Z, ===X.Y.Z
|
|
71
|
-
# then return the requirement as is
|
|
72
|
-
return requirement_string unless requirement_string.match?(/^=?={0,2}\s*\d+\.\d+(\.\d+)?(-[a-z0-9.-]+)?$/i)
|
|
73
|
-
|
|
74
|
-
parts = requirement_string.gsub(/^=+/, "").split(".")
|
|
75
|
-
|
|
76
|
-
case parts.length
|
|
77
|
-
when 1 # Only major version (X)
|
|
78
|
-
">= #{parts[0]}.0.0 < #{parts[0].to_i + 1}.0.0" # Ensure only major version range
|
|
79
|
-
when 2 # Major.Minor (X.Y)
|
|
80
|
-
">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Ensure only minor version range
|
|
81
|
-
when 3 # Major.Minor.Patch (X.Y.Z)
|
|
82
|
-
">= #{parts[0]}.#{parts[1]}.0 < #{parts[0].to_i}.#{parts[1].to_i + 1}.0" # Convert to >= X.Y.0
|
|
83
|
-
else
|
|
84
|
-
requirement_string
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
sig { returns(String) }
|
|
89
|
-
def python_version_from_supported_versions
|
|
90
|
-
requirement_string = python_requirement_string
|
|
91
|
-
|
|
92
|
-
# If the requirement string isn't already a range (eg ">3.10"), coerce it to "major.minor.*".
|
|
93
|
-
# The patch version is ignored because a non-matching patch version is unlikely to affect resolution.
|
|
94
|
-
requirement_string = requirement_string.gsub(/\.\d+$/, ".*") if /^\d/.match?(requirement_string)
|
|
95
|
-
|
|
96
|
-
requirement_string = normalize_python_exact_version(requirement_string)
|
|
97
|
-
|
|
98
|
-
if requirement_string.nil? || requirement_string.strip.empty?
|
|
99
|
-
return Language::PRE_INSTALLED_HIGHEST_VERSION.to_s
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Try to match one of our pre-installed Python versions
|
|
103
|
-
requirement = T.must(Requirement.requirements_array(requirement_string).first)
|
|
104
|
-
version = Language::PRE_INSTALLED_PYTHON_VERSIONS.find { |v| requirement.satisfied_by?(v) }
|
|
105
|
-
return version.to_s if version
|
|
106
|
-
|
|
107
|
-
# Otherwise we have to raise an error
|
|
108
|
-
supported_versions = Language::SUPPORTED_VERSIONS.map { |v| "#{v}.*" }.join(", ")
|
|
109
|
-
raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
sig { returns(T.untyped) }
|
|
113
|
-
def user_specified_python_version
|
|
114
|
-
@python_requirement_parser.user_specified_requirements.first
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
sig { returns(T.nilable(String)) }
|
|
118
|
-
def python_version_matching_imputed_requirements
|
|
119
|
-
compiled_file_python_requirement_markers =
|
|
120
|
-
@python_requirement_parser.imputed_requirements.map do |r|
|
|
121
|
-
Requirement.new(r)
|
|
122
|
-
end
|
|
123
|
-
python_version_matching(compiled_file_python_requirement_markers)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
sig { params(requirements: T.untyped).returns(T.nilable(String)) }
|
|
127
|
-
def python_version_matching(requirements)
|
|
128
|
-
Language::PRE_INSTALLED_PYTHON_VERSIONS.find do |version|
|
|
129
|
-
requirements.all? do |req|
|
|
130
|
-
next req.any? { |r| r.satisfied_by?(version) } if req.is_a?(Array)
|
|
131
|
-
|
|
132
|
-
req.satisfied_by?(version)
|
|
133
|
-
end
|
|
134
|
-
end.to_s
|
|
135
|
-
end
|
|
136
|
-
end
|
|
8
|
+
# Uv and Python ecosystems share the same Python version management logic.
|
|
9
|
+
# This alias ensures uv benefits from improvements in Python's implementation,
|
|
10
|
+
# including bug fixes like the guard clause in python_version_matching_imputed_requirements.
|
|
11
|
+
LanguageVersionManager = Python::LanguageVersionManager
|
|
137
12
|
end
|
|
138
13
|
end
|
|
@@ -1,220 +1,16 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "excon"
|
|
5
4
|
require "sorbet-runtime"
|
|
6
|
-
require "
|
|
7
|
-
|
|
5
|
+
require "dependabot/python/metadata_finder"
|
|
8
6
|
require "dependabot/metadata_finders"
|
|
9
|
-
require "dependabot/metadata_finders/base"
|
|
10
|
-
require "dependabot/registry_client"
|
|
11
|
-
require "dependabot/uv/authed_url_builder"
|
|
12
|
-
require "dependabot/uv/name_normaliser"
|
|
13
7
|
|
|
14
8
|
module Dependabot
|
|
15
9
|
module Uv
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
MAIN_PYPI_URL = "https://pypi.org/pypi"
|
|
20
|
-
|
|
21
|
-
sig do
|
|
22
|
-
params(
|
|
23
|
-
dependency: Dependabot::Dependency,
|
|
24
|
-
credentials: T::Array[Dependabot::Credential]
|
|
25
|
-
)
|
|
26
|
-
.void
|
|
27
|
-
end
|
|
28
|
-
def initialize(dependency:, credentials:)
|
|
29
|
-
super
|
|
30
|
-
@pypi_listing = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
|
31
|
-
@source_from_description = T.let(nil, T.nilable(String))
|
|
32
|
-
@source_from_homepage = T.let(nil, T.nilable(String))
|
|
33
|
-
@homepage_response = T.let(nil, T.nilable(Excon::Response))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
sig { returns(T.nilable(String)) }
|
|
37
|
-
def homepage_url
|
|
38
|
-
pypi_listing.dig("info", "home_page") ||
|
|
39
|
-
pypi_listing.dig("info", "project_urls", "Homepage") ||
|
|
40
|
-
pypi_listing.dig("info", "project_urls", "homepage") ||
|
|
41
|
-
super
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
sig { override.returns(T.nilable(Dependabot::Source)) }
|
|
47
|
-
def look_up_source
|
|
48
|
-
potential_source_urls = [
|
|
49
|
-
pypi_listing.dig("info", "project_urls", "Source"),
|
|
50
|
-
pypi_listing.dig("info", "project_urls", "Repository"),
|
|
51
|
-
pypi_listing.dig("info", "home_page"),
|
|
52
|
-
pypi_listing.dig("info", "download_url"),
|
|
53
|
-
pypi_listing.dig("info", "docs_url")
|
|
54
|
-
].compact
|
|
55
|
-
|
|
56
|
-
potential_source_urls +=
|
|
57
|
-
(pypi_listing.dig("info", "project_urls") || {}).values
|
|
58
|
-
|
|
59
|
-
source_url = potential_source_urls.find { |url| Source.from_url(url) }
|
|
60
|
-
source_url ||= source_from_description
|
|
61
|
-
source_url ||= source_from_homepage
|
|
62
|
-
|
|
63
|
-
Source.from_url(source_url)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
67
|
-
sig { returns(T.nilable(String)) }
|
|
68
|
-
def source_from_description
|
|
69
|
-
potential_source_urls = []
|
|
70
|
-
desc = pypi_listing.dig("info", "description")
|
|
71
|
-
return unless desc
|
|
72
|
-
|
|
73
|
-
desc.scan(Source::SOURCE_REGEX) do
|
|
74
|
-
potential_source_urls << Regexp.last_match.to_s
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Looking for a source where the repo name exactly matches the
|
|
78
|
-
# dependency name
|
|
79
|
-
match_url = potential_source_urls.find do |url|
|
|
80
|
-
repo = Source.from_url(url)&.repo
|
|
81
|
-
repo&.downcase&.end_with?(normalised_dependency_name)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
return match_url if match_url
|
|
85
|
-
|
|
86
|
-
# Failing that, look for a source where the full dependency name is
|
|
87
|
-
# mentioned when the link is followed
|
|
88
|
-
@source_from_description ||= T.let(
|
|
89
|
-
potential_source_urls.find do |url|
|
|
90
|
-
full_url = Source.from_url(url)&.url
|
|
91
|
-
next unless full_url
|
|
92
|
-
|
|
93
|
-
response = Dependabot::RegistryClient.get(url: full_url)
|
|
94
|
-
next unless response.status == 200
|
|
95
|
-
|
|
96
|
-
response.body.include?(normalised_dependency_name)
|
|
97
|
-
end,
|
|
98
|
-
T.nilable(String)
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
102
|
-
|
|
103
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
104
|
-
sig { returns(T.nilable(String)) }
|
|
105
|
-
def source_from_homepage
|
|
106
|
-
homepage_body_local = homepage_body
|
|
107
|
-
return unless homepage_body_local
|
|
108
|
-
|
|
109
|
-
potential_source_urls = []
|
|
110
|
-
homepage_body_local.scan(Source::SOURCE_REGEX) do
|
|
111
|
-
potential_source_urls << Regexp.last_match.to_s
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
match_url = potential_source_urls.find do |url|
|
|
115
|
-
repo = Source.from_url(url)&.repo
|
|
116
|
-
repo&.downcase&.end_with?(normalised_dependency_name)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
return match_url if match_url
|
|
120
|
-
|
|
121
|
-
@source_from_homepage ||= T.let(
|
|
122
|
-
potential_source_urls.find do |url|
|
|
123
|
-
full_url = Source.from_url(url)&.url
|
|
124
|
-
next unless full_url
|
|
125
|
-
|
|
126
|
-
response = Dependabot::RegistryClient.get(url: full_url)
|
|
127
|
-
next unless response.status == 200
|
|
128
|
-
|
|
129
|
-
response.body.include?(normalised_dependency_name)
|
|
130
|
-
end,
|
|
131
|
-
T.nilable(String)
|
|
132
|
-
)
|
|
133
|
-
end
|
|
134
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
135
|
-
|
|
136
|
-
sig { returns(T.nilable(String)) }
|
|
137
|
-
def homepage_body
|
|
138
|
-
homepage_url = pypi_listing.dig("info", "home_page")
|
|
139
|
-
|
|
140
|
-
return unless homepage_url
|
|
141
|
-
return if [
|
|
142
|
-
"pypi.org",
|
|
143
|
-
"pypi.python.org"
|
|
144
|
-
].include?(URI(homepage_url).host)
|
|
145
|
-
|
|
146
|
-
@homepage_response ||= T.let(
|
|
147
|
-
begin
|
|
148
|
-
Dependabot::RegistryClient.get(url: homepage_url)
|
|
149
|
-
rescue Excon::Error::Timeout, Excon::Error::Socket,
|
|
150
|
-
Excon::Error::TooManyRedirects, ArgumentError
|
|
151
|
-
nil
|
|
152
|
-
end,
|
|
153
|
-
T.nilable(Excon::Response)
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
return unless @homepage_response&.status == 200
|
|
157
|
-
|
|
158
|
-
@homepage_response&.body
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
|
162
|
-
def pypi_listing
|
|
163
|
-
return @pypi_listing unless @pypi_listing.nil?
|
|
164
|
-
return @pypi_listing = {} if dependency.version&.include?("+")
|
|
165
|
-
|
|
166
|
-
possible_listing_urls.each do |url|
|
|
167
|
-
response = fetch_authed_url(url)
|
|
168
|
-
next unless response.status == 200
|
|
169
|
-
|
|
170
|
-
@pypi_listing = JSON.parse(response.body)
|
|
171
|
-
return @pypi_listing
|
|
172
|
-
rescue JSON::ParserError
|
|
173
|
-
next
|
|
174
|
-
rescue Excon::Error::Timeout
|
|
175
|
-
next
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
@pypi_listing = {} # No listing found
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
sig { params(url: String).returns(Excon::Response) }
|
|
182
|
-
def fetch_authed_url(url)
|
|
183
|
-
if url.match(%r{(.*)://(.*?):(.*)@([^@]+)$}) &&
|
|
184
|
-
Regexp.last_match&.captures&.[](1)&.include?("@")
|
|
185
|
-
protocol, user, pass, url = T.must(Regexp.last_match).captures
|
|
186
|
-
|
|
187
|
-
Dependabot::RegistryClient.get(
|
|
188
|
-
url: "#{protocol}://#{url}",
|
|
189
|
-
options: {
|
|
190
|
-
user: user,
|
|
191
|
-
password: pass
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
else
|
|
195
|
-
Dependabot::RegistryClient.get(url: url)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
sig { returns(T::Array[String]) }
|
|
200
|
-
def possible_listing_urls
|
|
201
|
-
credential_urls =
|
|
202
|
-
credentials
|
|
203
|
-
.select { |cred| cred["type"] == "python_index" }
|
|
204
|
-
.map { |c| AuthedUrlBuilder.authed_url(credential: c) }
|
|
205
|
-
|
|
206
|
-
(credential_urls + [MAIN_PYPI_URL]).map do |base_url|
|
|
207
|
-
base_url.gsub(%r{/$}, "") + "/#{normalised_dependency_name}/json"
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Strip [extras] from name (dependency_name[extra_dep,other_extra])
|
|
212
|
-
sig { returns(String) }
|
|
213
|
-
def normalised_dependency_name
|
|
214
|
-
NameNormaliser.normalise(dependency.name)
|
|
215
|
-
end
|
|
216
|
-
end
|
|
10
|
+
# UV uses Python's PyPI metadata lookup, so we delegate to Python::MetadataFinder
|
|
11
|
+
MetadataFinder = Dependabot::Python::MetadataFinder
|
|
217
12
|
end
|
|
218
13
|
end
|
|
219
14
|
|
|
220
|
-
Dependabot::MetadataFinders
|
|
15
|
+
Dependabot::MetadataFinders
|
|
16
|
+
.register("uv", Dependabot::Uv::MetadataFinder)
|
|
@@ -2,25 +2,11 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/python/name_normaliser"
|
|
5
6
|
|
|
6
7
|
module Dependabot
|
|
7
8
|
module Uv
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sig { params(name: String).returns(String) }
|
|
12
|
-
def self.normalise(name)
|
|
13
|
-
extras_regex = /\[.+\]/
|
|
14
|
-
name.downcase.gsub(/[-_.]+/, "-").gsub(extras_regex, "")
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
sig { params(name: String, extras: T::Array[String]).returns(String) }
|
|
18
|
-
def self.normalise_including_extras(name, extras)
|
|
19
|
-
normalised_name = normalise(name)
|
|
20
|
-
return normalised_name if extras.empty?
|
|
21
|
-
|
|
22
|
-
normalised_name + "[" + extras.join(",") + "]"
|
|
23
|
-
end
|
|
24
|
-
end
|
|
9
|
+
# UV uses the same Python package name normalization (PEP 503)
|
|
10
|
+
NameNormaliser = Dependabot::Python::NameNormaliser
|
|
25
11
|
end
|
|
26
12
|
end
|
|
@@ -1,38 +1,12 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
4
|
+
require "dependabot/python/native_helpers"
|
|
5
5
|
|
|
6
6
|
module Dependabot
|
|
7
7
|
module Uv
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sig { returns(String) }
|
|
12
|
-
def self.python_helper_path
|
|
13
|
-
clean_path(File.join(python_helpers_dir, "run.py"))
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
sig { returns(String) }
|
|
17
|
-
def self.python_requirements_path
|
|
18
|
-
clean_path(File.join(python_helpers_dir, "requirements.txt"))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
sig { returns(String) }
|
|
22
|
-
def self.python_helpers_dir
|
|
23
|
-
File.join(native_helpers_root, "python")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
sig { returns(String) }
|
|
27
|
-
def self.native_helpers_root
|
|
28
|
-
default_path = File.join(__dir__, "../../../..")
|
|
29
|
-
ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
sig { params(path: T.nilable(String)).returns(String) }
|
|
33
|
-
def self.clean_path(path)
|
|
34
|
-
Pathname.new(path).cleanpath.to_path
|
|
35
|
-
end
|
|
36
|
-
end
|
|
8
|
+
# Uv and Python ecosystems share the same native Python helpers.
|
|
9
|
+
# Both point to the same helpers/python directory.
|
|
10
|
+
NativeHelpers = Python::NativeHelpers
|
|
37
11
|
end
|
|
38
12
|
end
|