dependabot-uv 0.350.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/lib/dependabot/uv/file_updater/lock_file_updater.rb +7 -1
- data/lib/dependabot/uv/language.rb +6 -76
- data/lib/dependabot/uv/language_version_manager.rb +6 -131
- data/lib/dependabot/uv/native_helpers.rb +4 -30
- data/lib/dependabot/uv/package.rb +27 -0
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +10 -16
- metadata +7 -8
- data/lib/dependabot/uv/package/package_details_fetcher.rb +0 -486
- data/lib/dependabot/uv/package/package_registry_finder.rb +0 -288
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
|
|
@@ -285,7 +285,13 @@ module Dependabot
|
|
|
285
285
|
options_fingerprint = lock_options_fingerprint(options)
|
|
286
286
|
|
|
287
287
|
# Use pyenv exec to ensure we're using the correct Python environment
|
|
288
|
-
|
|
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}"
|
|
289
295
|
fingerprint = "pyenv exec uv lock --upgrade-package <dependency_name> #{options_fingerprint}"
|
|
290
296
|
|
|
291
297
|
run_command(command, fingerprint:)
|
|
@@ -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,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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "dependabot/python/package/package_registry_finder"
|
|
6
|
+
require "dependabot/python/package/package_details_fetcher"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Uv
|
|
10
|
+
# UV uses the same Python package registry handling (PyPI)
|
|
11
|
+
module Package
|
|
12
|
+
# Re-export constants from Python::Package for backward compatibility
|
|
13
|
+
CREDENTIALS_USERNAME = Python::Package::CREDENTIALS_USERNAME
|
|
14
|
+
CREDENTIALS_PASSWORD = Python::Package::CREDENTIALS_PASSWORD
|
|
15
|
+
APPLICATION_JSON = Python::Package::APPLICATION_JSON
|
|
16
|
+
APPLICATION_TEXT = Python::Package::APPLICATION_TEXT
|
|
17
|
+
CPYTHON = Python::Package::CPYTHON
|
|
18
|
+
PYTHON = Python::Package::PYTHON
|
|
19
|
+
UNKNOWN = Python::Package::UNKNOWN
|
|
20
|
+
MAIN_PYPI_INDEXES = Python::Package::MAIN_PYPI_INDEXES
|
|
21
|
+
VERSION_REGEX = Python::Package::VERSION_REGEX
|
|
22
|
+
|
|
23
|
+
PackageRegistryFinder = Dependabot::Python::Package::PackageRegistryFinder
|
|
24
|
+
PackageDetailsFetcher = Dependabot::Python::Package::PackageDetailsFetcher
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "cgi"
|
|
5
|
-
require "excon"
|
|
6
|
-
require "nokogiri"
|
|
7
4
|
require "sorbet-runtime"
|
|
8
|
-
|
|
9
|
-
require "dependabot/dependency"
|
|
10
5
|
require "dependabot/uv/update_checker"
|
|
11
|
-
require "dependabot/
|
|
12
|
-
require "dependabot/registry_client"
|
|
13
|
-
require "dependabot/uv/authed_url_builder"
|
|
14
|
-
require "dependabot/uv/name_normaliser"
|
|
15
|
-
require "dependabot/uv/package/package_registry_finder"
|
|
16
|
-
require "dependabot/uv/package/package_details_fetcher"
|
|
6
|
+
require "dependabot/uv/package"
|
|
17
7
|
require "dependabot/package/package_latest_version_finder"
|
|
18
8
|
|
|
19
9
|
module Dependabot
|
|
20
10
|
module Uv
|
|
21
11
|
class UpdateChecker
|
|
12
|
+
# UV uses the same PyPI registry for package lookups as Python
|
|
22
13
|
class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
|
|
23
14
|
extend T::Sig
|
|
24
15
|
|
|
@@ -26,11 +17,14 @@ module Dependabot
|
|
|
26
17
|
override.returns(T.nilable(Dependabot::Package::PackageDetails))
|
|
27
18
|
end
|
|
28
19
|
def package_details
|
|
29
|
-
@package_details ||=
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
@package_details ||= T.let(
|
|
21
|
+
Package::PackageDetailsFetcher.new(
|
|
22
|
+
dependency: dependency,
|
|
23
|
+
dependency_files: dependency_files,
|
|
24
|
+
credentials: credentials
|
|
25
|
+
).fetch,
|
|
26
|
+
T.nilable(Dependabot::Package::PackageDetails)
|
|
27
|
+
)
|
|
34
28
|
end
|
|
35
29
|
|
|
36
30
|
sig { override.returns(T::Boolean) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-uv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.351.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.351.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.351.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: dependabot-python
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.351.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.351.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: debug
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -278,8 +278,7 @@ files:
|
|
|
278
278
|
- lib/dependabot/uv/metadata_finder.rb
|
|
279
279
|
- lib/dependabot/uv/name_normaliser.rb
|
|
280
280
|
- lib/dependabot/uv/native_helpers.rb
|
|
281
|
-
- lib/dependabot/uv/package
|
|
282
|
-
- lib/dependabot/uv/package/package_registry_finder.rb
|
|
281
|
+
- lib/dependabot/uv/package.rb
|
|
283
282
|
- lib/dependabot/uv/package_manager.rb
|
|
284
283
|
- lib/dependabot/uv/requirement.rb
|
|
285
284
|
- lib/dependabot/uv/requirement_parser.rb
|
|
@@ -296,7 +295,7 @@ licenses:
|
|
|
296
295
|
- MIT
|
|
297
296
|
metadata:
|
|
298
297
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
299
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
298
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.351.0
|
|
300
299
|
rdoc_options: []
|
|
301
300
|
require_paths:
|
|
302
301
|
- lib
|
|
@@ -1,486 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "json"
|
|
5
|
-
require "time"
|
|
6
|
-
require "cgi"
|
|
7
|
-
require "excon"
|
|
8
|
-
require "nokogiri"
|
|
9
|
-
require "sorbet-runtime"
|
|
10
|
-
require "dependabot/registry_client"
|
|
11
|
-
require "dependabot/uv/name_normaliser"
|
|
12
|
-
require "dependabot/package/package_release"
|
|
13
|
-
require "dependabot/package/package_details"
|
|
14
|
-
require "dependabot/uv/package/package_registry_finder"
|
|
15
|
-
|
|
16
|
-
# Stores metadata for a package, including all its available versions
|
|
17
|
-
module Dependabot
|
|
18
|
-
module Uv
|
|
19
|
-
module Package
|
|
20
|
-
CREDENTIALS_USERNAME = "username"
|
|
21
|
-
CREDENTIALS_PASSWORD = "password"
|
|
22
|
-
|
|
23
|
-
APPLICATION_JSON = "application/json"
|
|
24
|
-
APPLICATION_TEXT = "text/html"
|
|
25
|
-
CPYTHON = "cpython"
|
|
26
|
-
PYTHON = "python"
|
|
27
|
-
UNKNOWN = "unknown"
|
|
28
|
-
|
|
29
|
-
MAIN_PYPI_INDEXES = %w(
|
|
30
|
-
https://pypi.python.org/simple/
|
|
31
|
-
https://pypi.org/simple/
|
|
32
|
-
).freeze
|
|
33
|
-
VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/
|
|
34
|
-
|
|
35
|
-
class PackageDetailsFetcher
|
|
36
|
-
extend T::Sig
|
|
37
|
-
|
|
38
|
-
sig do
|
|
39
|
-
params(
|
|
40
|
-
dependency: Dependabot::Dependency,
|
|
41
|
-
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
42
|
-
credentials: T::Array[Dependabot::Credential]
|
|
43
|
-
).void
|
|
44
|
-
end
|
|
45
|
-
def initialize(
|
|
46
|
-
dependency:,
|
|
47
|
-
dependency_files:,
|
|
48
|
-
credentials:
|
|
49
|
-
)
|
|
50
|
-
@dependency = dependency
|
|
51
|
-
@dependency_files = dependency_files
|
|
52
|
-
@credentials = credentials
|
|
53
|
-
|
|
54
|
-
@registry_urls = T.let(nil, T.nilable(T::Array[String]))
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
sig { returns(Dependabot::Dependency) }
|
|
58
|
-
attr_reader :dependency
|
|
59
|
-
|
|
60
|
-
sig { returns(T::Array[T.untyped]) }
|
|
61
|
-
attr_reader :dependency_files
|
|
62
|
-
|
|
63
|
-
sig { returns(T::Array[T.untyped]) }
|
|
64
|
-
attr_reader :credentials
|
|
65
|
-
|
|
66
|
-
sig { returns(Dependabot::Package::PackageDetails) }
|
|
67
|
-
def fetch
|
|
68
|
-
package_releases = registry_urls
|
|
69
|
-
.select { |index_url| validate_index(index_url) } # Ensure only valid URLs
|
|
70
|
-
.flat_map do |index_url|
|
|
71
|
-
fetch_from_registry(index_url) || [] # Ensure it always returns an array
|
|
72
|
-
rescue Excon::Error::Timeout, Excon::Error::Socket
|
|
73
|
-
raise if MAIN_PYPI_INDEXES.include?(index_url)
|
|
74
|
-
|
|
75
|
-
raise PrivateSourceTimedOut, sanitized_url(index_url)
|
|
76
|
-
rescue URI::InvalidURIError
|
|
77
|
-
raise DependencyFileNotResolvable, "Invalid URL: #{sanitized_url(index_url)}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
Dependabot::Package::PackageDetails.new(
|
|
81
|
-
dependency: dependency,
|
|
82
|
-
releases: package_releases.reverse
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
sig do
|
|
89
|
-
params(index_url: String)
|
|
90
|
-
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
|
91
|
-
end
|
|
92
|
-
def fetch_from_registry(index_url)
|
|
93
|
-
metadata = fetch_from_json_registry(index_url)
|
|
94
|
-
|
|
95
|
-
return metadata if metadata&.any?
|
|
96
|
-
|
|
97
|
-
Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
|
|
98
|
-
fetch_from_html_registry(index_url)
|
|
99
|
-
rescue StandardError => e
|
|
100
|
-
Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
|
|
101
|
-
fetch_from_html_registry(index_url)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Example JSON Response Format:
|
|
105
|
-
#
|
|
106
|
-
# {
|
|
107
|
-
# "info": {
|
|
108
|
-
# "name": "requests",
|
|
109
|
-
# "summary": "Python HTTP for Humans.",
|
|
110
|
-
# "author": "Kenneth Reitz",
|
|
111
|
-
# "license": "Apache-2.0"
|
|
112
|
-
# },
|
|
113
|
-
# "releases": {
|
|
114
|
-
# "2.32.3": [
|
|
115
|
-
# {
|
|
116
|
-
# "filename": "requests-2.32.3-py3-none-any.whl",
|
|
117
|
-
# "version": "2.32.3",
|
|
118
|
-
# "requires_python": ">=3.8",
|
|
119
|
-
# "yanked": false,
|
|
120
|
-
# "url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl"
|
|
121
|
-
# },
|
|
122
|
-
# {
|
|
123
|
-
# "filename": "requests-2.32.3.tar.gz",
|
|
124
|
-
# "version": "2.32.3",
|
|
125
|
-
# "requires_python": ">=3.8",
|
|
126
|
-
# "yanked": false,
|
|
127
|
-
# "url": "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz"
|
|
128
|
-
# }
|
|
129
|
-
# ],
|
|
130
|
-
# "2.27.0": [
|
|
131
|
-
# {
|
|
132
|
-
# "filename": "requests-2.27.0-py2.py3-none-any.whl",
|
|
133
|
-
# "version": "2.27.0",
|
|
134
|
-
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
|
135
|
-
# "yanked": false,
|
|
136
|
-
# "url": "https://files.pythonhosted.org/packages/47/01/f420e7add78110940639a958e5af0e3f8e07a8a8b62049bac55ee117aa91/requests-2.27.0-py2.py3-none-any.whl"
|
|
137
|
-
# },
|
|
138
|
-
# {
|
|
139
|
-
# "filename": "requests-2.27.0.tar.gz",
|
|
140
|
-
# "version": "2.27.0",
|
|
141
|
-
# "requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
|
|
142
|
-
# "yanked": false,
|
|
143
|
-
# "url": "https://files.pythonhosted.org/packages/c0/e3/826e27b942352a74b656e8f58b4dc7ed9495ce2d4eeb498181167c615303/requests-2.27.0.tar.gz"
|
|
144
|
-
# }
|
|
145
|
-
# ]
|
|
146
|
-
# }
|
|
147
|
-
# }
|
|
148
|
-
sig do
|
|
149
|
-
params(index_url: String)
|
|
150
|
-
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
|
151
|
-
end
|
|
152
|
-
def fetch_from_json_registry(index_url)
|
|
153
|
-
json_url = index_url.sub(%r{/simple/?$}i, "/pypi/")
|
|
154
|
-
|
|
155
|
-
Dependabot.logger.info(
|
|
156
|
-
"Fetching release information from json registry at #{sanitized_url(json_url)} for #{dependency.name}"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
response = registry_json_response_for_dependency(json_url)
|
|
160
|
-
|
|
161
|
-
return nil unless response.status == 200
|
|
162
|
-
|
|
163
|
-
begin
|
|
164
|
-
data = JSON.parse(response.body)
|
|
165
|
-
|
|
166
|
-
version_releases = data["releases"]
|
|
167
|
-
|
|
168
|
-
releases = format_version_releases(version_releases)
|
|
169
|
-
|
|
170
|
-
releases.sort_by(&:version).reverse
|
|
171
|
-
rescue JSON::ParserError
|
|
172
|
-
Dependabot.logger.warn("JSON parsing error for #{json_url}. Falling back to HTML.")
|
|
173
|
-
nil
|
|
174
|
-
rescue StandardError => e
|
|
175
|
-
Dependabot.logger.warn("Unexpected error while fetching JSON data: #{e.message}.")
|
|
176
|
-
nil
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# This URL points to the Simple Index API for the "requests" package on PyPI.
|
|
181
|
-
# It provides an HTML listing of available package versions following PEP 503 (Simple Repository API).
|
|
182
|
-
# The information found here is useful for dependency resolution and package version retrieval.
|
|
183
|
-
#
|
|
184
|
-
# ✅ Information available in the Simple Index:
|
|
185
|
-
# - A list of package versions as anchor (`<a>`) elements.
|
|
186
|
-
# - URLs to distribution files (e.g., `.tar.gz`, `.whl`).
|
|
187
|
-
# - The `data-requires-python` attribute (if present) specifying the required Python version.
|
|
188
|
-
# - An optional `data-yanked` attribute indicating a yanked (withdrawn) version.
|
|
189
|
-
#
|
|
190
|
-
# ❌ Information NOT available in the Simple Index:
|
|
191
|
-
# - Release timestamps (upload time).
|
|
192
|
-
# - File digests (hashes like SHA256, MD5).
|
|
193
|
-
# - Package metadata such as description, author, or dependencies.
|
|
194
|
-
# - Download statistics.
|
|
195
|
-
# - Package type (`sdist` or `bdist_wheel`).
|
|
196
|
-
#
|
|
197
|
-
# To obtain full package metadata, use the PyPI JSON API:
|
|
198
|
-
# - JSON API: https://pypi.org/pypi/requests/json
|
|
199
|
-
#
|
|
200
|
-
# More details: https://www.python.org/dev/peps/pep-0503/
|
|
201
|
-
sig { params(index_url: String).returns(T::Array[Dependabot::Package::PackageRelease]) }
|
|
202
|
-
def fetch_from_html_registry(index_url)
|
|
203
|
-
Dependabot.logger.info(
|
|
204
|
-
"Fetching release information from html registry at #{sanitized_url(index_url)} for #{dependency.name}"
|
|
205
|
-
)
|
|
206
|
-
index_response = registry_response_for_dependency(index_url)
|
|
207
|
-
if index_response.status == 401 || index_response.status == 403
|
|
208
|
-
registry_index_response = registry_index_response(index_url)
|
|
209
|
-
|
|
210
|
-
if registry_index_response.status == 401 || registry_index_response.status == 403
|
|
211
|
-
raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
version_releases = extract_release_details_json_from_html(index_response.body)
|
|
216
|
-
releases = format_version_releases(version_releases)
|
|
217
|
-
|
|
218
|
-
releases.sort_by(&:version).reverse
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
sig do
|
|
222
|
-
params(html_body: String)
|
|
223
|
-
.returns(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]) # Returns JSON-like format
|
|
224
|
-
end
|
|
225
|
-
def extract_release_details_json_from_html(html_body)
|
|
226
|
-
doc = Nokogiri::HTML(html_body)
|
|
227
|
-
|
|
228
|
-
releases = {}
|
|
229
|
-
|
|
230
|
-
doc.css("a").each do |a_tag|
|
|
231
|
-
details = version_details_from_link(a_tag.to_s)
|
|
232
|
-
if details && details["version"]
|
|
233
|
-
releases[details["version"]] ||= []
|
|
234
|
-
releases[details["version"]] << details
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
releases
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
242
|
-
sig do
|
|
243
|
-
params(link: T.nilable(String))
|
|
244
|
-
.returns(T.nilable(T::Hash[String, T.untyped]))
|
|
245
|
-
end
|
|
246
|
-
def version_details_from_link(link)
|
|
247
|
-
return unless link
|
|
248
|
-
|
|
249
|
-
doc = Nokogiri::XML(link)
|
|
250
|
-
filename = doc.at_css("a")&.content
|
|
251
|
-
url = doc.at_css("a")&.attributes&.fetch("href", nil)&.value
|
|
252
|
-
|
|
253
|
-
return unless filename&.match?(name_regex) || url&.match?(name_regex)
|
|
254
|
-
|
|
255
|
-
version = get_version_from_filename(filename)
|
|
256
|
-
return unless version_class.correct?(version)
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
"version" => version,
|
|
260
|
-
"requires_python" => requires_python_from_link(link),
|
|
261
|
-
"yanked" => link.include?("data-yanked"),
|
|
262
|
-
"url" => link
|
|
263
|
-
}
|
|
264
|
-
end
|
|
265
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
266
|
-
|
|
267
|
-
sig do
|
|
268
|
-
params(
|
|
269
|
-
releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]])
|
|
270
|
-
)
|
|
271
|
-
.returns(T::Array[Dependabot::Package::PackageRelease])
|
|
272
|
-
end
|
|
273
|
-
def format_version_releases(releases_json)
|
|
274
|
-
return [] unless releases_json
|
|
275
|
-
|
|
276
|
-
releases_json.each_with_object([]) do |(version, release_data_array), versions|
|
|
277
|
-
release_data = release_data_array.last
|
|
278
|
-
|
|
279
|
-
next unless release_data
|
|
280
|
-
|
|
281
|
-
release = format_version_release(version, release_data)
|
|
282
|
-
|
|
283
|
-
next unless release
|
|
284
|
-
|
|
285
|
-
versions << release
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
sig do
|
|
290
|
-
params(
|
|
291
|
-
version: String,
|
|
292
|
-
release_data: T::Hash[String, T.untyped]
|
|
293
|
-
)
|
|
294
|
-
.returns(T.nilable(Dependabot::Package::PackageRelease))
|
|
295
|
-
end
|
|
296
|
-
def format_version_release(version, release_data)
|
|
297
|
-
upload_time = release_data["upload_time"]
|
|
298
|
-
released_at = Time.parse(upload_time) if upload_time
|
|
299
|
-
yanked = release_data["yanked"] || false
|
|
300
|
-
yanked_reason = release_data["yanked_reason"]
|
|
301
|
-
downloads = release_data["downloads"] || -1
|
|
302
|
-
url = release_data["url"]
|
|
303
|
-
package_type = release_data["packagetype"]
|
|
304
|
-
language = package_language(
|
|
305
|
-
python_version: release_data["python_version"],
|
|
306
|
-
requires_python: release_data["requires_python"]
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
release = Dependabot::Package::PackageRelease.new(
|
|
310
|
-
version: Dependabot::Uv::Version.new(version),
|
|
311
|
-
released_at: released_at,
|
|
312
|
-
yanked: yanked,
|
|
313
|
-
yanked_reason: yanked_reason,
|
|
314
|
-
downloads: downloads,
|
|
315
|
-
url: url,
|
|
316
|
-
package_type: package_type,
|
|
317
|
-
language: language
|
|
318
|
-
)
|
|
319
|
-
release
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
sig do
|
|
323
|
-
params(
|
|
324
|
-
python_version: T.nilable(String),
|
|
325
|
-
requires_python: T.nilable(String)
|
|
326
|
-
)
|
|
327
|
-
.returns(T.nilable(Dependabot::Package::PackageLanguage))
|
|
328
|
-
end
|
|
329
|
-
def package_language(python_version:, requires_python:)
|
|
330
|
-
# Extract language name and version
|
|
331
|
-
language_name, language_version = convert_language_version(python_version)
|
|
332
|
-
|
|
333
|
-
# Extract language requirement
|
|
334
|
-
language_requirement = build_python_requirement(requires_python)
|
|
335
|
-
|
|
336
|
-
return nil unless language_version || language_requirement
|
|
337
|
-
|
|
338
|
-
# Return a Language object with all details
|
|
339
|
-
Dependabot::Package::PackageLanguage.new(
|
|
340
|
-
name: language_name,
|
|
341
|
-
version: language_version,
|
|
342
|
-
requirement: language_requirement
|
|
343
|
-
)
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
sig { params(version: T.nilable(String)).returns([String, T.nilable(Dependabot::Version)]) }
|
|
347
|
-
def convert_language_version(version)
|
|
348
|
-
return ["python", nil] if version.nil? || version == "source"
|
|
349
|
-
|
|
350
|
-
# Extract numeric parts dynamically (e.g., "cp37" -> "3.7", "py38" -> "3.8")
|
|
351
|
-
extracted_version = version.scan(/\d+/).join(".")
|
|
352
|
-
|
|
353
|
-
# Detect the language implementation
|
|
354
|
-
language_name = if version.start_with?("cp")
|
|
355
|
-
"cpython" # CPython implementation
|
|
356
|
-
elsif version.start_with?("py")
|
|
357
|
-
"python" # General Python compatibility
|
|
358
|
-
else
|
|
359
|
-
"unknown" # Fallback for unknown cases
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Ensure extracted version is valid before converting
|
|
363
|
-
language_version =
|
|
364
|
-
extracted_version.match?(/^\d+(\.\d+)*$/) ? Dependabot::Version.new(extracted_version) : nil
|
|
365
|
-
|
|
366
|
-
Dependabot.logger.warn("Skipping invalid language_version: #{version.inspect}") if language_version.nil?
|
|
367
|
-
|
|
368
|
-
[language_name, language_version]
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
sig { returns(T::Array[String]) }
|
|
372
|
-
def registry_urls
|
|
373
|
-
@registry_urls ||=
|
|
374
|
-
Package::PackageRegistryFinder.new(
|
|
375
|
-
dependency_files: dependency_files,
|
|
376
|
-
credentials: credentials,
|
|
377
|
-
dependency: dependency
|
|
378
|
-
).registry_urls
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
sig { returns(String) }
|
|
382
|
-
def normalised_name
|
|
383
|
-
NameNormaliser.normalise(dependency.name)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
sig { params(json_url: String).returns(Excon::Response) }
|
|
387
|
-
def registry_json_response_for_dependency(json_url)
|
|
388
|
-
url = "#{json_url.chomp('/')}/#{@dependency.name}/json"
|
|
389
|
-
Dependabot::RegistryClient.get(
|
|
390
|
-
url: url,
|
|
391
|
-
headers: { "Accept" => APPLICATION_JSON }
|
|
392
|
-
)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
sig { params(index_url: String).returns(Excon::Response) }
|
|
396
|
-
def registry_response_for_dependency(index_url)
|
|
397
|
-
Dependabot::RegistryClient.get(
|
|
398
|
-
url: index_url + normalised_name + "/",
|
|
399
|
-
headers: { "Accept" => APPLICATION_TEXT }
|
|
400
|
-
)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
sig { params(index_url: String).returns(Excon::Response) }
|
|
404
|
-
def registry_index_response(index_url)
|
|
405
|
-
Dependabot::RegistryClient.get(
|
|
406
|
-
url: index_url,
|
|
407
|
-
headers: { "Accept" => APPLICATION_TEXT }
|
|
408
|
-
)
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
sig { params(filename: String).returns(T.nilable(String)) }
|
|
412
|
-
def get_version_from_filename(filename)
|
|
413
|
-
filename
|
|
414
|
-
.gsub(/#{name_regex}-/i, "")
|
|
415
|
-
.split(/-|\.tar\.|\.zip|\.whl/)
|
|
416
|
-
.first
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
sig do
|
|
420
|
-
params(req_string: T.nilable(String))
|
|
421
|
-
.returns(T.nilable(Dependabot::Requirement))
|
|
422
|
-
end
|
|
423
|
-
def build_python_requirement(req_string)
|
|
424
|
-
return nil unless req_string
|
|
425
|
-
|
|
426
|
-
requirement_class.new(CGI.unescapeHTML(req_string))
|
|
427
|
-
rescue Gem::Requirement::BadRequirementError
|
|
428
|
-
nil
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
sig { params(link: String).returns(T.nilable(String)) }
|
|
432
|
-
def requires_python_from_link(link)
|
|
433
|
-
raw_value = Nokogiri::XML(link)
|
|
434
|
-
.at_css("a")
|
|
435
|
-
&.attribute("data-requires-python")
|
|
436
|
-
&.content
|
|
437
|
-
|
|
438
|
-
return nil unless raw_value
|
|
439
|
-
|
|
440
|
-
CGI.unescapeHTML(raw_value) # Decodes HTML entities like >=3 → >=3
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
sig { returns(T.class_of(Dependabot::Version)) }
|
|
444
|
-
def version_class
|
|
445
|
-
dependency.version_class
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
sig { returns(T.class_of(Dependabot::Requirement)) }
|
|
449
|
-
def requirement_class
|
|
450
|
-
dependency.requirement_class
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
sig { params(index_url: String).returns(T::Hash[String, String]) }
|
|
454
|
-
def auth_headers_for(index_url)
|
|
455
|
-
credential = @credentials.find { |cred| cred["index-url"] == index_url }
|
|
456
|
-
return {} unless credential
|
|
457
|
-
|
|
458
|
-
{ "Authorization" => "Basic #{Base64.strict_encode64(
|
|
459
|
-
"#{credential[CREDENTIALS_USERNAME]}:#{credential[CREDENTIALS_PASSWORD]}"
|
|
460
|
-
)}" }
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
sig { returns(Regexp) }
|
|
464
|
-
def name_regex
|
|
465
|
-
parts = normalised_name.split(/[\s_.-]/).map { |n| Regexp.quote(n) }
|
|
466
|
-
/#{parts.join("[\s_.-]")}/i
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
sig { params(index_url: T.nilable(String)).returns(T::Boolean) }
|
|
470
|
-
def validate_index(index_url)
|
|
471
|
-
return false unless index_url
|
|
472
|
-
|
|
473
|
-
return true if index_url.match?(URI::RFC2396_PARSER.regexp[:ABS_URI])
|
|
474
|
-
|
|
475
|
-
raise Dependabot::DependencyFileNotResolvable,
|
|
476
|
-
"Invalid URL: #{sanitized_url(index_url)}"
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
sig { params(index_url: String).returns(String) }
|
|
480
|
-
def sanitized_url(index_url)
|
|
481
|
-
index_url.sub(%r{//([^/@]+)@}, "//redacted@")
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
end
|
|
486
|
-
end
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "toml-rb"
|
|
5
|
-
require "sorbet-runtime"
|
|
6
|
-
require "dependabot/dependency"
|
|
7
|
-
require "dependabot/dependency_file"
|
|
8
|
-
require "dependabot/credential"
|
|
9
|
-
require "dependabot/uv/update_checker"
|
|
10
|
-
require "dependabot/uv/authed_url_builder"
|
|
11
|
-
require "dependabot/errors"
|
|
12
|
-
|
|
13
|
-
module Dependabot
|
|
14
|
-
module Uv
|
|
15
|
-
module Package
|
|
16
|
-
class PackageRegistryFinder
|
|
17
|
-
extend T::Sig
|
|
18
|
-
|
|
19
|
-
PYPI_BASE_URL = "https://pypi.org/simple/"
|
|
20
|
-
ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
|
|
21
|
-
|
|
22
|
-
UrlsHash = T.type_alias { { main: T.nilable(String), extra: T::Array[String] } }
|
|
23
|
-
|
|
24
|
-
sig do
|
|
25
|
-
params(
|
|
26
|
-
dependency_files: T::Array[Dependabot::DependencyFile],
|
|
27
|
-
credentials: T::Array[Dependabot::Credential],
|
|
28
|
-
dependency: Dependabot::Dependency
|
|
29
|
-
).void
|
|
30
|
-
end
|
|
31
|
-
def initialize(dependency_files:, credentials:, dependency:)
|
|
32
|
-
@dependency_files = T.let(dependency_files, T::Array[Dependabot::DependencyFile])
|
|
33
|
-
@credentials = T.let(credentials, T::Array[Dependabot::Credential])
|
|
34
|
-
@dependency = T.let(dependency, Dependabot::Dependency)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
sig { returns(T::Array[String]) }
|
|
38
|
-
def registry_urls
|
|
39
|
-
extra_index_urls =
|
|
40
|
-
config_variable_index_urls[:extra] +
|
|
41
|
-
pipfile_index_urls[:extra] +
|
|
42
|
-
requirement_file_index_urls[:extra] +
|
|
43
|
-
pip_conf_index_urls[:extra] +
|
|
44
|
-
pyproject_index_urls[:extra]
|
|
45
|
-
|
|
46
|
-
extra_index_urls = extra_index_urls.map do |url|
|
|
47
|
-
clean_check_and_remove_environment_variables(url)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# URL encode any `@` characters within registry URL creds.
|
|
51
|
-
# TODO: The test that fails if the `map` here is removed is likely a
|
|
52
|
-
# bug in Ruby's URI parser, and should be fixed there.
|
|
53
|
-
[main_index_url, *extra_index_urls].map do |url|
|
|
54
|
-
url.rpartition("@").tap { |a| a.first.gsub!("@", "%40") }.join
|
|
55
|
-
end.uniq
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
61
|
-
attr_reader :dependency_files
|
|
62
|
-
|
|
63
|
-
sig { returns(T::Array[Dependabot::Credential]) }
|
|
64
|
-
attr_reader :credentials
|
|
65
|
-
|
|
66
|
-
sig { returns(String) }
|
|
67
|
-
def main_index_url
|
|
68
|
-
url =
|
|
69
|
-
config_variable_index_urls[:main] ||
|
|
70
|
-
pipfile_index_urls[:main] ||
|
|
71
|
-
requirement_file_index_urls[:main] ||
|
|
72
|
-
pip_conf_index_urls[:main] ||
|
|
73
|
-
pyproject_index_urls[:main] ||
|
|
74
|
-
PYPI_BASE_URL
|
|
75
|
-
|
|
76
|
-
clean_check_and_remove_environment_variables(url)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
sig { returns(UrlsHash) }
|
|
80
|
-
def requirement_file_index_urls
|
|
81
|
-
urls = T.let({ main: nil, extra: [] }, UrlsHash)
|
|
82
|
-
|
|
83
|
-
requirements_files.each do |file|
|
|
84
|
-
content = file.content
|
|
85
|
-
next unless content
|
|
86
|
-
|
|
87
|
-
if content.match?(/^--index-url\s+['"]?([^\s'"]+)['"]?/)
|
|
88
|
-
match_result = content.match(/^--index-url\s+['"]?([^\s'"]+)['"]?/)
|
|
89
|
-
urls[:main] = match_result&.captures&.first&.strip
|
|
90
|
-
end
|
|
91
|
-
extra_urls = urls[:extra]
|
|
92
|
-
extra_urls +=
|
|
93
|
-
content
|
|
94
|
-
.scan(/^--extra-index-url\s+['"]?([^\s'"]+)['"]?/)
|
|
95
|
-
.flatten
|
|
96
|
-
.map(&:strip)
|
|
97
|
-
urls[:extra] = extra_urls
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
urls
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
sig { returns(UrlsHash) }
|
|
104
|
-
def pip_conf_index_urls
|
|
105
|
-
urls = T.let({ main: nil, extra: [] }, UrlsHash)
|
|
106
|
-
|
|
107
|
-
return urls unless pip_conf
|
|
108
|
-
|
|
109
|
-
pip_conf_file = pip_conf
|
|
110
|
-
return urls unless pip_conf_file
|
|
111
|
-
|
|
112
|
-
content = pip_conf_file.content
|
|
113
|
-
return urls unless content
|
|
114
|
-
|
|
115
|
-
if content.match?(/^index-url\s*=/x)
|
|
116
|
-
match_result = content.match(/^index-url\s*=\s*(.+)/)
|
|
117
|
-
urls[:main] = match_result&.captures&.first
|
|
118
|
-
end
|
|
119
|
-
extra_urls = urls[:extra]
|
|
120
|
-
extra_urls += content.scan(/^extra-index-url\s*=(.+)/).flatten
|
|
121
|
-
urls[:extra] = extra_urls
|
|
122
|
-
|
|
123
|
-
urls
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
sig { returns(UrlsHash) }
|
|
127
|
-
def pipfile_index_urls
|
|
128
|
-
urls = T.let({ main: nil, extra: [] }, UrlsHash)
|
|
129
|
-
begin
|
|
130
|
-
return urls unless pipfile
|
|
131
|
-
|
|
132
|
-
pipfile_file = pipfile
|
|
133
|
-
return urls unless pipfile_file
|
|
134
|
-
|
|
135
|
-
content = pipfile_file.content
|
|
136
|
-
return urls unless content
|
|
137
|
-
|
|
138
|
-
pipfile_object = TomlRB.parse(content)
|
|
139
|
-
|
|
140
|
-
urls[:main] = pipfile_object["source"]&.first&.fetch("url", nil)
|
|
141
|
-
|
|
142
|
-
pipfile_object["source"]&.each do |source|
|
|
143
|
-
urls[:extra] << source.fetch("url") if source["url"]
|
|
144
|
-
end
|
|
145
|
-
urls[:extra] = urls[:extra].uniq
|
|
146
|
-
|
|
147
|
-
urls
|
|
148
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
149
|
-
urls
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
154
|
-
sig { returns(UrlsHash) }
|
|
155
|
-
def pyproject_index_urls
|
|
156
|
-
urls = T.let({ main: nil, extra: [] }, UrlsHash)
|
|
157
|
-
|
|
158
|
-
begin
|
|
159
|
-
return urls unless pyproject
|
|
160
|
-
|
|
161
|
-
pyproject_file = pyproject
|
|
162
|
-
return urls unless pyproject_file
|
|
163
|
-
|
|
164
|
-
pyproject_content = pyproject_file.content
|
|
165
|
-
return urls unless pyproject_content
|
|
166
|
-
|
|
167
|
-
sources =
|
|
168
|
-
TomlRB.parse(pyproject_content).dig("tool", "poetry", "source") ||
|
|
169
|
-
[]
|
|
170
|
-
|
|
171
|
-
sources.each do |source|
|
|
172
|
-
# If source is PyPI, skip it, and let it pick the default URI
|
|
173
|
-
next if source["name"].casecmp?("PyPI")
|
|
174
|
-
|
|
175
|
-
if @dependency.all_sources.include?(source["name"])
|
|
176
|
-
# If dependency has specified this source, use it
|
|
177
|
-
return { main: source["url"], extra: [] }
|
|
178
|
-
elsif source["default"]
|
|
179
|
-
urls[:main] = source["url"]
|
|
180
|
-
elsif source["priority"] != "explicit"
|
|
181
|
-
# if source is not explicit, add it to extra
|
|
182
|
-
urls[:extra] << source["url"]
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
urls[:extra] = urls[:extra].uniq
|
|
186
|
-
|
|
187
|
-
urls
|
|
188
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
189
|
-
urls
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
193
|
-
|
|
194
|
-
sig { returns(UrlsHash) }
|
|
195
|
-
def config_variable_index_urls
|
|
196
|
-
urls = T.let({ main: nil, extra: [] }, UrlsHash)
|
|
197
|
-
|
|
198
|
-
index_url_creds = credentials
|
|
199
|
-
.select { |cred| cred["type"] == "python_index" }
|
|
200
|
-
|
|
201
|
-
if (main_cred = index_url_creds.find(&:replaces_base?))
|
|
202
|
-
urls[:main] = AuthedUrlBuilder.authed_url(credential: main_cred)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
urls[:extra] =
|
|
206
|
-
index_url_creds
|
|
207
|
-
.reject(&:replaces_base?)
|
|
208
|
-
.map { |cred| AuthedUrlBuilder.authed_url(credential: cred) }
|
|
209
|
-
|
|
210
|
-
urls
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
sig { params(url: String).returns(String) }
|
|
214
|
-
def clean_check_and_remove_environment_variables(url)
|
|
215
|
-
url = url.strip.sub(%r{/+$}, "") + "/"
|
|
216
|
-
|
|
217
|
-
return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
|
|
218
|
-
|
|
219
|
-
config_variable_urls =
|
|
220
|
-
[
|
|
221
|
-
config_variable_index_urls[:main],
|
|
222
|
-
*config_variable_index_urls[:extra]
|
|
223
|
-
]
|
|
224
|
-
.compact
|
|
225
|
-
.map { |u| u.strip.gsub(%r{/*$}, "") + "/" }
|
|
226
|
-
|
|
227
|
-
regexp = url
|
|
228
|
-
.sub(%r{(?<=://).+@}, "")
|
|
229
|
-
.sub(%r{https?://}, "")
|
|
230
|
-
.split(ENVIRONMENT_VARIABLE_REGEX)
|
|
231
|
-
.map { |part| Regexp.quote(part) }
|
|
232
|
-
.join(".+")
|
|
233
|
-
authed_url = config_variable_urls.find { |u| u.match?(regexp) }
|
|
234
|
-
return authed_url if authed_url
|
|
235
|
-
|
|
236
|
-
cleaned_url = url.gsub(%r{#{ENVIRONMENT_VARIABLE_REGEX}/?}o, "")
|
|
237
|
-
authed_url = authed_base_url(cleaned_url)
|
|
238
|
-
return authed_url if credential_for(cleaned_url)
|
|
239
|
-
|
|
240
|
-
raise PrivateSourceAuthenticationFailure, url
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
sig { params(base_url: String).returns(String) }
|
|
244
|
-
def authed_base_url(base_url)
|
|
245
|
-
cred = credential_for(base_url)
|
|
246
|
-
return base_url unless cred
|
|
247
|
-
|
|
248
|
-
AuthedUrlBuilder.authed_url(credential: cred).gsub(%r{/*$}, "") + "/"
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
sig { params(url: String).returns(T.nilable(Dependabot::Credential)) }
|
|
252
|
-
def credential_for(url)
|
|
253
|
-
credentials
|
|
254
|
-
.select { |c| c["type"] == "python_index" }
|
|
255
|
-
.find do |c|
|
|
256
|
-
cred_url = c.fetch("index-url").gsub(%r{/*$}, "") + "/"
|
|
257
|
-
cred_url.include?(url)
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
262
|
-
def pip_conf
|
|
263
|
-
dependency_files.find { |f| f.name == "pip.conf" }
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
267
|
-
def pipfile
|
|
268
|
-
dependency_files.find { |f| f.name == "Pipfile" }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
272
|
-
def pyproject
|
|
273
|
-
dependency_files.find { |f| f.name == "pyproject.toml" }
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
277
|
-
def requirements_files
|
|
278
|
-
dependency_files.select { |f| f.name.match?(/requirements/x) }
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
282
|
-
def pip_compile_files
|
|
283
|
-
dependency_files.select { |f| f.name.end_with?(".in") }
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
end
|