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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55b564d1c18b3b690b6bd6b857c47e618ec6f6350d316642ddcd33150daa8181
4
- data.tar.gz: 0e5737a7ae48bd53c845c6346eccb2a74ae34a6b6a71d8a2ec885387479cca71
3
+ metadata.gz: f74cec4813c7f6b11b51b6f73a10db68327ca41a2737e469ba5d9ea3b4612bea
4
+ data.tar.gz: e17b701d94aeafdca486c2303d23ea3b423eebbddd93d347ce1224160920cab5
5
5
  SHA512:
6
- metadata.gz: 60b62b77c2f49a0c1a8993cf37af124492cf17c5185478fe121fbf9ae594d5d3cda5481039487806384ca4f86f250bafd914fc3379498635140ac62eda6a493c
7
- data.tar.gz: 4873f91475324508c1bb7b3fc24af31561bdef46488249c3e94d6d252ac0812d2465fdfd0563b839cd2526eafe474fcac1a4361880ee463b90e5a708df83646d
6
+ metadata.gz: 32c427d81b22edb5d8b145ced9080a7fca9264e371da8f4a9ff5ca57c89b58f69179b4ade15620e24bb6a5d43194d959489d0c10942b050be8fb5e0c13327121
7
+ data.tar.gz: 615a296656b8957f897e5b17e3f951b143579bbc08aa8e8c806928bcd3e3349e4f0e3a54f0dcf3b5449fa7c8b6a892677e9a865925ec1fb044094ee35f4de527
@@ -7,7 +7,7 @@ plette==2.1.0
7
7
  poetry==1.8.5
8
8
  # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
9
9
  tomli==2.0.1
10
- uv==0.9.8
10
+ uv==0.9.11
11
11
 
12
12
  # Some dependencies will only install if Cython is present
13
13
  Cython==3.0.10
@@ -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
- class AuthedUrlBuilder
9
- extend T::Sig
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
- declaration_regex = declaration_regex(dep, old_r)
132
- declaration_match = content.match(declaration_regex)
133
- if declaration_match
134
- declaration = declaration_match[:declaration]
135
- new_declaration = T.must(declaration).sub(old_req, new_req)
136
- content.sub(T.must(declaration), new_declaration)
137
- else
138
- content
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
- 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}"
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 "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,220 +1,16 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "excon"
5
4
  require "sorbet-runtime"
6
- require "uri"
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
- class MetadataFinder < Dependabot::MetadataFinders::Base
17
- extend T::Sig
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.register("uv", Dependabot::Uv::MetadataFinder)
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
- module NameNormaliser
9
- extend T::Sig
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 "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