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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b23c5beac181aafb8e92f670984121a88254da3a1804dbd7891e9e6bc274b84c
4
- data.tar.gz: c5007c5452b38eac6210eac2df08635c69e3ed7fb7410e8173e9964678902d9b
3
+ metadata.gz: f74cec4813c7f6b11b51b6f73a10db68327ca41a2737e469ba5d9ea3b4612bea
4
+ data.tar.gz: e17b701d94aeafdca486c2303d23ea3b423eebbddd93d347ce1224160920cab5
5
5
  SHA512:
6
- metadata.gz: f646e21b2dd5f869a1fc56a48ab0f04051d202b4efc6f98a4d3dd2efca28526ffae66068ee3d59fd2ad5e03ad243db060ddcc703df018cefb4c1f06369043887
7
- data.tar.gz: e5044b7cd9b6bb4f3a8273309e1d20a0ffb784aa5c5b92401f28e0a885cef9ac239ee2e4a51fdff6978425644529e705a835b8d58932bd45aae92e47d9dfc3e5
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
- command = "pyenv exec uv lock --upgrade-package #{T.must(dependency).name} #{options}"
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 "sorbet-runtime"
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
- LANGUAGE = "python"
11
-
12
- class Language < Dependabot::Ecosystem::VersionManager
13
- extend T::Sig
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: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "dependabot/logger"
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
- class LanguageVersionManager
11
- extend T::Sig
12
-
13
- sig { params(python_requirement_parser: T.untyped).void }
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 "sorbet-runtime"
4
+ require "dependabot/python/native_helpers"
5
5
 
6
6
  module Dependabot
7
7
  module Uv
8
- module NativeHelpers
9
- extend T::Sig
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/update_checkers/version_filters"
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 ||= Package::PackageDetailsFetcher.new(
30
- dependency: dependency,
31
- dependency_files: dependency_files,
32
- credentials: credentials
33
- ).fetch
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.350.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.350.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.350.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.350.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.350.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/package_details_fetcher.rb
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.350.0
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 &gt;=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