dependabot-uv 0.349.0 → 0.350.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: b23c5beac181aafb8e92f670984121a88254da3a1804dbd7891e9e6bc274b84c
4
+ data.tar.gz: c5007c5452b38eac6210eac2df08635c69e3ed7fb7410e8173e9964678902d9b
5
5
  SHA512:
6
- metadata.gz: 60b62b77c2f49a0c1a8993cf37af124492cf17c5185478fe121fbf9ae594d5d3cda5481039487806384ca4f86f250bafd914fc3379498635140ac62eda6a493c
7
- data.tar.gz: 4873f91475324508c1bb7b3fc24af31561bdef46488249c3e94d6d252ac0812d2465fdfd0563b839cd2526eafe474fcac1a4361880ee463b90e5a708df83646d
6
+ metadata.gz: f646e21b2dd5f869a1fc56a48ab0f04051d202b4efc6f98a4d3dd2efca28526ffae66068ee3d59fd2ad5e03ad243db060ddcc703df018cefb4c1f06369043887
7
+ data.tar.gz: e5044b7cd9b6bb4f3a8273309e1d20a0ffb784aa5c5b92401f28e0a885cef9ac239ee2e4a51fdff6978425644529e705a835b8d58932bd45aae92e47d9dfc3e5
@@ -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
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(",")
139
160
  end
161
+
162
+ normalize.call(req1) == normalize.call(req2)
140
163
  end
141
164
 
142
165
  sig { returns(String) }
@@ -313,24 +336,6 @@ module Dependabot
313
336
  url.gsub(%r{^https?://}, "").gsub(/[^a-zA-Z0-9]/, "_").upcase
314
337
  end
315
338
 
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
339
  sig { returns(String) }
335
340
  def lock_options
336
341
  options = lock_index_options
@@ -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,206 +1,14 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
-
6
- require "dependabot/requirement"
5
+ require "dependabot/python/requirement"
7
6
  require "dependabot/utils"
8
- require "dependabot/uv/version"
9
7
 
10
8
  module Dependabot
11
9
  module Uv
12
- class Requirement < Dependabot::Requirement
13
- extend T::Sig
14
-
15
- OR_SEPARATOR = T.let(/(?<=[a-zA-Z0-9)*])\s*\|+/, Regexp)
16
-
17
- # Add equality and arbitrary-equality matchers
18
- OPS = T.let(
19
- OPS.merge(
20
- "==" => ->(v, r) { v == r },
21
- "===" => ->(v, r) { v.to_s == r.to_s }
22
- ),
23
- T::Hash[String, T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)]
24
- )
25
-
26
- quoted = OPS.keys.sort_by(&:length).reverse
27
- .map { |k| Regexp.quote(k) }.join("|")
28
- version_pattern = Uv::Version::VERSION_PATTERN
29
-
30
- PATTERN_RAW = T.let("\\s*(?<op>#{quoted})?\\s*(?<version>#{version_pattern})\\s*".freeze, String)
31
- PATTERN = T.let(/\A#{PATTERN_RAW}\z/, Regexp)
32
- PARENS_PATTERN = T.let(/\A\(([^)]+)\)\z/, Regexp)
33
-
34
- sig { params(obj: T.any(Gem::Version, String)).returns([String, Gem::Version]) }
35
- def self.parse(obj)
36
- return ["=", Uv::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
37
-
38
- line = obj.to_s
39
- if (matches = PARENS_PATTERN.match(line))
40
- line = matches[1]
41
- end
42
-
43
- unless (matches = PATTERN.match(line))
44
- msg = "Illformed requirement [#{obj.inspect}]"
45
- raise BadRequirementError, msg
46
- end
47
-
48
- return DefaultRequirement if matches[:op] == ">=" && matches[:version] == "0"
49
-
50
- [matches[:op] || "=", Uv::Version.new(T.must(matches[:version]))]
51
- end
52
-
53
- # Returns an array of requirements. At least one requirement from the
54
- # returned array must be satisfied for a version to be valid.
55
- #
56
- # NOTE: Or requirements are only valid for Poetry.
57
- sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
58
- def self.requirements_array(requirement_string)
59
- return [new(nil)] if requirement_string.nil?
60
-
61
- if (matches = PARENS_PATTERN.match(requirement_string))
62
- requirement_string = matches[1]
63
- end
64
-
65
- T.must(requirement_string).strip.split(OR_SEPARATOR).map do |req_string|
66
- new(req_string.strip)
67
- end
68
- end
69
-
70
- sig { params(requirements: T.nilable(T.any(String, T::Array[String]))).void }
71
- def initialize(*requirements)
72
- requirements = requirements.flatten.flat_map do |req_string|
73
- next if req_string.nil?
74
-
75
- # Standard python doesn't support whitespace in requirements, but Poetry does.
76
- req_string = req_string.gsub(/(\d +)([<=>])/, '\1,\2')
77
-
78
- req_string.split(",").map(&:strip).map do |r|
79
- convert_python_constraint_to_ruby_constraint(r)
80
- end
81
- end
82
-
83
- super(requirements)
84
- end
85
-
86
- sig { params(version: T.any(Gem::Version, String)).returns(T::Boolean) }
87
- def satisfied_by?(version)
88
- version = Uv::Version.new(version.to_s)
89
-
90
- requirements.all? { |op, rv| T.must(OPS[op] || OPS["="]).call(version, rv) }
91
- end
92
-
93
- sig { returns(T::Boolean) }
94
- def exact?
95
- return false unless requirements.size == 1
96
-
97
- %w(= == ===).include?(requirements[0][0])
98
- end
99
-
100
- private
101
-
102
- sig { params(req_string: T.nilable(String)).returns(T.nilable(T.any(String, T::Array[String]))) }
103
- def convert_python_constraint_to_ruby_constraint(req_string)
104
- return nil if req_string.nil? || req_string.strip.empty?
105
- return nil if req_string == "*"
106
-
107
- req_string = req_string.gsub("~=", "~>")
108
- req_string = req_string.gsub(/(?<=\d)[<=>].*\Z/, "")
109
-
110
- if req_string.match?(/~[^>]/) then convert_tilde_req(req_string)
111
- elsif req_string.start_with?("^") then convert_caret_req(req_string)
112
- elsif req_string.match?(/^=?={0,2}\s*\d+\.\d+(\.\d+)?(-[a-z0-9.-]+)?(\.\*)?$/i)
113
- convert_exact(req_string)
114
- elsif req_string.include?(".*") then convert_wildcard(req_string)
115
- else
116
- req_string
117
- end
118
- end
119
-
120
- # Poetry uses ~ requirements.
121
- # https://github.com/sdispater/poetry#tilde-requirements
122
- sig { params(req_string: String).returns(String) }
123
- def convert_tilde_req(req_string)
124
- version = req_string.gsub(/^~\>?/, "")
125
- parts = version.split(".")
126
- parts << "0" if parts.count < 3
127
- "~> #{parts.join('.')}"
128
- end
129
-
130
- # Poetry uses ^ requirements
131
- # https://github.com/sdispater/poetry#caret-requirement
132
- sig { params(req_string: String).returns(T::Array[String]) }
133
- def convert_caret_req(req_string)
134
- version = req_string.gsub(/^\^/, "")
135
- parts = version.split(".")
136
- parts.fill("0", parts.length...3)
137
- first_non_zero = parts.find { |d| d != "0" }
138
- first_non_zero_index =
139
- first_non_zero ? parts.index(first_non_zero) : parts.count - 1
140
- upper_bound = parts.map.with_index do |part, i|
141
- if i < T.must(first_non_zero_index) then part
142
- elsif i == first_non_zero_index then (part.to_i + 1).to_s
143
- # .dev has lowest precedence: https://packaging.python.org/en/latest/specifications/version-specifiers/#summary-of-permitted-suffixes-and-relative-ordering
144
- elsif i > T.must(first_non_zero_index) && i == 2 then "0.dev"
145
- else
146
- "0"
147
- end
148
- end.join(".")
149
-
150
- [">= #{version}", "< #{upper_bound}"]
151
- end
152
-
153
- sig { params(req_string: String).returns(String) }
154
- def convert_wildcard(req_string)
155
- # NOTE: This isn't perfect. It replaces the "!= 1.0.*" case with
156
- # "!= 1.0.0". There's no way to model this correctly in Ruby :'(
157
- quoted_ops = OPS.keys.sort_by(&:length).reverse
158
- .map { |k| Regexp.quote(k) }.join("|")
159
- op_match = req_string.match(/\A\s*(#{quoted_ops})?/)
160
- op = op_match&.captures&.first.to_s.strip
161
- exact_op = ["", "=", "==", "==="].include?(op)
162
-
163
- req_string.strip
164
- .split(".")
165
- .first(T.must(req_string.split(".").index { |s| s.include?("*") }) + 1)
166
- .join(".")
167
- .gsub(/\*(?!$)/, "0")
168
- .gsub(/\*$/, "0.dev")
169
- .tap { |s| exact_op ? s.gsub!(/^(?<!!)=*/, "~>") : s }
170
- end
171
-
172
- sig { params(req_string: String).returns(T.any(String, T::Array[String])) }
173
- def convert_exact(req_string)
174
- arbitrary_equality = req_string.start_with?("===")
175
- cleaned_version = req_string.gsub(/^=+/, "").strip
176
-
177
- return ["=== #{cleaned_version}"] if arbitrary_equality
178
-
179
- # Handle versions wildcarded with .*, e.g. 1.0.*
180
- if cleaned_version.include?(".*")
181
- # Remove all characters after the first .*, and the .*
182
- cleaned_version = cleaned_version.split(".*").first
183
- version = Version.new(cleaned_version)
184
- # Get the release segment parts [major, minor, patch]
185
- version_parts = version.release_segment
186
-
187
- if version_parts.length == 1
188
- major = T.must(version_parts[0])
189
- [">= #{major}.0.0.dev", "< #{major + 1}.0.0"]
190
- elsif version_parts.length == 2
191
- major, minor = version_parts
192
- "~> #{major}.#{minor}.0.dev"
193
- elsif version_parts.length == 3
194
- major, minor, patch = version_parts
195
- "~> #{major}.#{minor}.#{patch}.dev"
196
- else
197
- "= #{cleaned_version}"
198
- end
199
- else
200
- "= #{cleaned_version}"
201
- end
202
- end
203
- end
10
+ # UV uses Python's requirement scheme, so we delegate to Python::Requirement
11
+ Requirement = Dependabot::Python::Requirement
204
12
  end
205
13
  end
206
14
 
@@ -1,60 +1,12 @@
1
1
  # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+ require "dependabot/python/requirement_parser"
6
+
4
7
  module Dependabot
5
8
  module Uv
6
- class RequirementParser
7
- NAME = /[a-zA-Z0-9](?:[a-zA-Z0-9\-_\.]*[a-zA-Z0-9])?/
8
- EXTRA = /[a-zA-Z0-9\-_\.]+/
9
- COMPARISON = /===|==|>=|<=|<|>|~=|!=/
10
- VERSION = /([1-9][0-9]*!)?[0-9]+[a-zA-Z0-9\-_.*]*(\+[0-9a-zA-Z]+(\.[0-9a-zA-Z]+)*)?/
11
-
12
- REQUIREMENT = /(?<comparison>#{COMPARISON})\s*\\?\s*v?(?<version>#{VERSION})/
13
- HASH = /--hash=(?<algorithm>.*?):(?<hash>.*?)(?=\s|\\|$)/
14
- REQUIREMENTS = /#{REQUIREMENT}(\s*,\s*\\?\s*#{REQUIREMENT})*/
15
- HASHES = /#{HASH}(\s*\\?\s*#{HASH})*/
16
- MARKER_OP = /\s*(#{COMPARISON}|(\s*in)|(\s*not\s*in))/
17
- PYTHON_STR_C = %r{[a-zA-Z0-9\s\(\)\.\{\}\-_\*#:;/\?\[\]!~`@\$%\^&=\+\|<>]}
18
- PYTHON_STR = /('(#{PYTHON_STR_C}|")*'|"(#{PYTHON_STR_C}|')*")/
19
- ENV_VAR =
20
- /python_version|python_full_version|os_name|sys_platform|
21
- platform_release|platform_system|platform_version|platform_machine|
22
- platform_python_implementation|implementation_name|
23
- implementation_version/
24
- MARKER_VAR = /\s*(#{ENV_VAR}|#{PYTHON_STR})/
25
- MARKER_EXPR_ONE = /#{MARKER_VAR}#{MARKER_OP}#{MARKER_VAR}/
26
- MARKER_EXPR = /(#{MARKER_EXPR_ONE}|\(\s*|\s*\)|\s+and\s+|\s+or\s+)+/
27
-
28
- INSTALL_REQ_WITH_REQUIREMENT =
29
- /\s*\\?\s*(?<name>#{NAME})
30
- \s*\\?\s*(\[\s*(?<extras>#{EXTRA}(\s*,\s*#{EXTRA})*)\s*\])?
31
- \s*\\?\s*\(?(?<requirements>#{REQUIREMENTS})\)?
32
- \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
33
- \s*\\?\s*(?<hashes>#{HASHES})?
34
- \s*#*\s*(?<comment>.+)?
35
- /x
36
-
37
- INSTALL_REQ_WITHOUT_REQUIREMENT =
38
- /^\s*\\?\s*(?<name>#{NAME})
39
- \s*\\?\s*(\[\s*(?<extras>#{EXTRA}(\s*,\s*#{EXTRA})*)\s*\])?
40
- \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
41
- \s*\\?\s*(?<hashes>#{HASHES})?
42
- \s*#*\s*(?<comment>.+)?$
43
- /x
44
-
45
- VALID_REQ_TXT_REQUIREMENT =
46
- /^\s*\\?\s*(?<name>#{NAME})
47
- \s*\\?\s*(\[\s*(?<extras>#{EXTRA}(\s*,\s*#{EXTRA})*)\s*\])?
48
- \s*\\?\s*\(?(?<requirements>#{REQUIREMENTS})?\)?
49
- \s*\\?\s*(;\s*(?<markers>#{MARKER_EXPR}))?
50
- \s*\\?\s*(?<hashes>#{HASHES})?
51
- \s*(\#+\s*(?<comment>.*))?$
52
- /x
53
-
54
- NAME_WITH_EXTRAS =
55
- /\s*\\?\s*(?<name>#{NAME})
56
- (\s*\\?\s*\[\s*(?<extras>#{EXTRA}(\s*,\s*#{EXTRA})*)\s*\])?
57
- /x
58
- end
9
+ # UV uses the same Python requirement parsing regex patterns (PEP 508)
10
+ RequirementParser = Dependabot::Python::RequirementParser
59
11
  end
60
12
  end
@@ -1,331 +1,14 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
-
6
- require "dependabot/version"
5
+ require "dependabot/python/version"
7
6
  require "dependabot/utils"
8
7
 
9
- # See https://packaging.python.org/en/latest/specifications/version-specifiers for spec details.
10
-
11
8
  module Dependabot
12
9
  module Uv
13
- class Version < Dependabot::Version
14
- extend T::Sig
15
-
16
- sig { returns(Integer) }
17
- attr_reader :epoch
18
-
19
- sig { returns(T::Array[Integer]) }
20
- attr_reader :release_segment
21
-
22
- sig { returns(T.nilable(T::Array[T.any(String, Integer)])) }
23
- attr_reader :dev
24
-
25
- sig { returns(T.nilable(T::Array[T.any(String, Integer)])) }
26
- attr_reader :pre
27
-
28
- sig { returns(T.nilable(T::Array[T.any(String, Integer)])) }
29
- attr_reader :post
30
-
31
- sig { returns(T.nilable(T::Array[T.any(String, Integer)])) }
32
- attr_reader :local
33
-
34
- INFINITY = T.let(1000, Integer)
35
- NEGATIVE_INFINITY = T.let(-INFINITY, Integer)
36
-
37
- # See https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
38
- VERSION_PATTERN = /
39
- v?
40
- (?:
41
- (?:(?<epoch>[0-9]+)!)? # epoch
42
- (?<release>[0-9]+(?:\.[0-9]+)*) # release
43
- (?<pre> # prerelease
44
- [-_\.]?
45
- (?<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
46
- [-_\.]?
47
- (?<pre_n>[0-9]+)?
48
- )?
49
- (?<post> # post release
50
- (?:-(?<post_n1>[0-9]+))
51
- |
52
- (?:
53
- [-_\.]?
54
- (?<post_l>post|rev|r)
55
- [-_\.]?
56
- (?<post_n2>[0-9]+)?
57
- )
58
- )?
59
- (?<dev> # dev release
60
- [-_\.]?
61
- (?<dev_l>dev)
62
- [-_\.]?
63
- (?<dev_n>[0-9]+)?
64
- )?
65
- )
66
- (?:\+(?<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
67
- /ix
68
-
69
- ANCHORED_VERSION_PATTERN = /\A\s*#{VERSION_PATTERN}\s*\z/
70
-
71
- sig { override.params(version: VersionParameter).returns(T::Boolean) }
72
- def self.correct?(version)
73
- return false if version.nil?
74
-
75
- version.to_s.match?(ANCHORED_VERSION_PATTERN)
76
- end
77
-
78
- sig { override.params(version: VersionParameter).void }
79
- def initialize(version) # rubocop:disable Metrics/AbcSize
80
- raise Dependabot::BadRequirementError, "Malformed version string - string is nil" if version.nil?
81
-
82
- @version_string = T.let(version.to_s, String)
83
-
84
- raise Dependabot::BadRequirementError, "Malformed version string - string is empty" if @version_string.empty?
85
-
86
- matches = ANCHORED_VERSION_PATTERN.match(@version_string.downcase)
87
-
88
- unless matches
89
- raise Dependabot::BadRequirementError,
90
- "Malformed version string - #{@version_string} does not match regex"
91
- end
92
-
93
- @epoch = T.let(matches["epoch"].to_i, Integer)
94
- @release_segment = T.let(matches["release"]&.split(".")&.map(&:to_i) || [], T::Array[Integer])
95
- @pre = T.let(
96
- parse_letter_version(matches["pre_l"], matches["pre_n"]),
97
- T.nilable(T::Array[T.any(String, Integer)])
98
- )
99
- @post = T.let(
100
- parse_letter_version(matches["post_l"], matches["post_n1"] || matches["post_n2"]),
101
- T.nilable(T::Array[T.any(String, Integer)])
102
- )
103
- @dev = T.let(
104
- parse_letter_version(matches["dev_l"], matches["dev_n"]),
105
- T.nilable(T::Array[T.any(String, Integer)])
106
- )
107
- @local = T.let(parse_local_version(matches["local"]), T.nilable(T::Array[T.any(String, Integer)]))
108
- super(matches["release"] || "")
109
- end
110
-
111
- sig { override.params(version: VersionParameter).returns(Dependabot::Uv::Version) }
112
- def self.new(version)
113
- T.cast(super, Dependabot::Uv::Version)
114
- end
115
-
116
- sig { returns(String) }
117
- def to_s
118
- @version_string
119
- end
120
-
121
- sig { returns(String) }
122
- def inspect # :nodoc:
123
- "#<#{self.class} #{@version_string}>"
124
- end
125
-
126
- sig { returns(T::Boolean) }
127
- def prerelease?
128
- !!(pre || dev)
129
- end
130
-
131
- sig { returns(Dependabot::Uv::Version) }
132
- def release
133
- Dependabot::Uv::Version.new(release_segment.join("."))
134
- end
135
-
136
- sig { params(other: VersionParameter).returns(Integer) }
137
- def <=>(other)
138
- other = Dependabot::Uv::Version.new(other.to_s) unless other.is_a?(Dependabot::Uv::Version)
139
-
140
- epoch_comparison = epoch <=> other.epoch
141
- return epoch_comparison unless epoch_comparison.zero?
142
-
143
- release_comparison = release_version_comparison(other)
144
- return release_comparison unless release_comparison.zero?
145
-
146
- pre_comparison = compare_keys(pre_cmp_key, other.pre_cmp_key)
147
- return pre_comparison unless pre_comparison.zero?
148
-
149
- post_comparison = compare_keys(post_cmp_key, other.post_cmp_key)
150
- return post_comparison unless post_comparison.zero?
151
-
152
- dev_comparison = compare_keys(dev_cmp_key, other.dev_cmp_key)
153
- return dev_comparison unless dev_comparison.zero?
154
-
155
- compare_keys(local_cmp_key, other.local_cmp_key)
156
- end
157
-
158
- sig do
159
- params(
160
- key: T.any(Integer, T::Array[T.any(String, Integer)]),
161
- other_key: T.any(Integer, T::Array[T.any(String, Integer)])
162
- ).returns(Integer)
163
- end
164
- def compare_keys(key, other_key)
165
- if key.is_a?(Integer) && other_key.is_a?(Integer)
166
- key <=> other_key
167
- elsif key.is_a?(Array) && other_key.is_a?(Array)
168
- key <=> other_key
169
- elsif key.is_a?(Integer)
170
- key == NEGATIVE_INFINITY ? -1 : 1
171
- elsif other_key.is_a?(Integer)
172
- other_key == NEGATIVE_INFINITY ? 1 : -1
173
- end
174
- end
175
-
176
- sig { returns(T.any(Integer, T::Array[T.any(String, Integer)])) }
177
- def pre_cmp_key
178
- if pre.nil? && post.nil? && dev # sort 1.0.dev0 before 1.0a0
179
- NEGATIVE_INFINITY
180
- elsif pre.nil?
181
- INFINITY # versions without a pre-release should sort after those with one.
182
- else
183
- T.must(pre)
184
- end
185
- end
186
-
187
- sig { returns(T.any(Integer, T::Array[T.any(String, Integer)])) }
188
- def local_cmp_key
189
- if local.nil?
190
- # Versions without a local segment should sort before those with one.
191
- NEGATIVE_INFINITY
192
- else
193
- # According to PEP440.
194
- # - Alphanumeric segments sort before numeric segments
195
- # - Alphanumeric segments sort lexicographically
196
- # - Numeric segments sort numerically
197
- # - Shorter versions sort before longer versions when the prefixes match exactly
198
- T.must(local).map do |token|
199
- if token.is_a?(Integer)
200
- [token, ""]
201
- else
202
- [NEGATIVE_INFINITY, token]
203
- end
204
- end.flatten
205
- end
206
- end
207
-
208
- sig { returns(T.any(Integer, T::Array[T.any(String, Integer)])) }
209
- def post_cmp_key
210
- # Versions without a post segment should sort before those with one.
211
- return NEGATIVE_INFINITY if post.nil?
212
-
213
- T.must(post)
214
- end
215
-
216
- sig { returns(T.any(Integer, T::Array[T.any(String, Integer)])) }
217
- def dev_cmp_key
218
- # Versions without a dev segment should sort after those with one.
219
- return INFINITY if dev.nil?
220
-
221
- T.must(dev)
222
- end
223
-
224
- sig { returns(String) }
225
- def lowest_prerelease_suffix
226
- "dev0"
227
- end
228
-
229
- sig { override.returns(T::Array[String]) }
230
- def ignored_patch_versions
231
- parts = release_segment # e.g [1,2,3] if version is 1.2.3-alpha3
232
- version_parts = parts.fill(0, parts.length...2)
233
- upper_parts = version_parts.first(1) + [version_parts[1].to_i + 1] + [lowest_prerelease_suffix]
234
- lower_bound = "> #{self}"
235
- upper_bound = "< #{upper_parts.join('.')}"
236
-
237
- ["#{lower_bound}, #{upper_bound}"]
238
- end
239
-
240
- sig { override.returns(T::Array[String]) }
241
- def ignored_minor_versions
242
- parts = release_segment # e.g [1,2,3] if version is 1.2.3-alpha3
243
- version_parts = parts.fill(0, parts.length...2)
244
- lower_parts = version_parts.first(1) + [version_parts[1].to_i + 1] + [lowest_prerelease_suffix]
245
- upper_parts = version_parts.first(0) + [version_parts[0].to_i + 1] + [lowest_prerelease_suffix]
246
- lower_bound = ">= #{lower_parts.join('.')}"
247
- upper_bound = "< #{upper_parts.join('.')}"
248
-
249
- ["#{lower_bound}, #{upper_bound}"]
250
- end
251
-
252
- sig { override.returns(T::Array[String]) }
253
- def ignored_major_versions
254
- version_parts = release_segment # e.g [1,2,3] if version is 1.2.3-alpha3
255
- lower_parts = [version_parts[0].to_i + 1] + [lowest_prerelease_suffix] # earliest next major version prerelease
256
- lower_bound = ">= #{lower_parts.join('.')}"
257
-
258
- [lower_bound]
259
- end
260
-
261
- private
262
-
263
- sig { params(other: Dependabot::Uv::Version).returns(Integer) }
264
- def release_version_comparison(other)
265
- tokens, other_tokens = pad_for_comparison(release_segment, other.release_segment)
266
- tokens <=> other_tokens
267
- end
268
-
269
- sig do
270
- params(
271
- tokens: T::Array[Integer],
272
- other_tokens: T::Array[Integer]
273
- ).returns(T::Array[T::Array[Integer]])
274
- end
275
- def pad_for_comparison(tokens, other_tokens)
276
- tokens = tokens.dup
277
- other_tokens = other_tokens.dup
278
-
279
- longer = [tokens, other_tokens].max_by(&:count)
280
- shorter = [tokens, other_tokens].min_by(&:count)
281
-
282
- difference = T.must(longer).length - T.must(shorter).length
283
-
284
- difference.times { T.must(shorter) << 0 }
285
-
286
- [tokens, other_tokens]
287
- end
288
-
289
- sig { params(local: T.nilable(String)).returns(T.nilable(T::Array[T.any(String, Integer)])) }
290
- def parse_local_version(local)
291
- return if local.nil?
292
-
293
- # Takes a string like abc.1.twelve and turns it into ["abc", 1, "twelve"]
294
- local.split(/[\._-]/).map { |s| /^\d+$/.match?(s) ? s.to_i : s }
295
- end
296
-
297
- sig do
298
- params(
299
- letter: T.nilable(String), number: T.nilable(String)
300
- ).returns(T.nilable(T::Array[T.any(String, Integer)]))
301
- end
302
- def parse_letter_version(letter = nil, number = nil)
303
- return if letter.nil? && number.nil?
304
-
305
- if letter
306
- # Implicit 0 for cases where prerelease has no numeral
307
- number ||= 0
308
-
309
- # Normalize alternate spellings
310
- if letter == "alpha"
311
- letter = "a"
312
- elsif letter == "beta"
313
- letter = "b"
314
- elsif %w(c pre preview).include? letter
315
- letter = "rc"
316
- elsif %w(rev r).include? letter
317
- letter = "post"
318
- end
319
-
320
- return letter, number.to_i
321
- end
322
-
323
- # Number but no letter i.e. implicit post release syntax (e.g. 1.0-1)
324
- letter = "post"
325
-
326
- [letter, number.to_i]
327
- end
328
- end
10
+ # UV uses Python's version scheme, so we delegate to Python::Version
11
+ Version = Dependabot::Python::Version
329
12
  end
330
13
  end
331
14
 
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.349.0
4
+ version: 0.350.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.349.0
18
+ version: 0.350.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.349.0
25
+ version: 0.350.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: dependabot-python
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.350.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.350.0
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: debug
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -253,7 +267,6 @@ files:
253
267
  - lib/dependabot/uv/file_parser.rb
254
268
  - lib/dependabot/uv/file_parser/pyproject_files_parser.rb
255
269
  - lib/dependabot/uv/file_parser/python_requirement_parser.rb
256
- - lib/dependabot/uv/file_parser/setup_file_parser.rb
257
270
  - lib/dependabot/uv/file_updater.rb
258
271
  - lib/dependabot/uv/file_updater/compile_file_updater.rb
259
272
  - lib/dependabot/uv/file_updater/lock_file_updater.rb
@@ -268,7 +281,6 @@ files:
268
281
  - lib/dependabot/uv/package/package_details_fetcher.rb
269
282
  - lib/dependabot/uv/package/package_registry_finder.rb
270
283
  - lib/dependabot/uv/package_manager.rb
271
- - lib/dependabot/uv/pipenv_runner.rb
272
284
  - lib/dependabot/uv/requirement.rb
273
285
  - lib/dependabot/uv/requirement_parser.rb
274
286
  - lib/dependabot/uv/requirements_file_matcher.rb
@@ -284,7 +296,7 @@ licenses:
284
296
  - MIT
285
297
  metadata:
286
298
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
287
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.349.0
299
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.350.0
288
300
  rdoc_options: []
289
301
  require_paths:
290
302
  - lib
@@ -1,194 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "dependabot/dependency"
5
- require "dependabot/errors"
6
- require "dependabot/file_parsers/base/dependency_set"
7
- require "dependabot/shared_helpers"
8
- require "dependabot/uv/file_parser"
9
- require "dependabot/uv/native_helpers"
10
- require "dependabot/uv/name_normaliser"
11
- require "sorbet-runtime"
12
-
13
- module Dependabot
14
- module Uv
15
- class FileParser
16
- class SetupFileParser
17
- extend T::Sig
18
-
19
- INSTALL_REQUIRES_REGEX = /install_requires\s*=\s*\[/m
20
- SETUP_REQUIRES_REGEX = /setup_requires\s*=\s*\[/m
21
- TESTS_REQUIRE_REGEX = /tests_require\s*=\s*\[/m
22
- EXTRAS_REQUIRE_REGEX = /extras_require\s*=\s*\{/m
23
-
24
- CLOSING_BRACKET = T.let({ "[" => "]", "{" => "}" }.freeze, T.any(T.untyped, T.untyped))
25
-
26
- sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
27
- def initialize(dependency_files:)
28
- @dependency_files = dependency_files
29
- end
30
-
31
- sig { returns(Dependabot::FileParsers::Base::DependencySet) }
32
- def dependency_set
33
- dependencies = Dependabot::FileParsers::Base::DependencySet.new
34
-
35
- parsed_setup_file.each do |dep|
36
- # If a requirement has a `<` or `<=` marker then updating it is
37
- # probably blocked. Ignore it.
38
- next if dep["markers"].include?("<")
39
-
40
- # If the requirement is our inserted version, ignore it
41
- # (we wouldn't be able to update it)
42
- next if dep["version"] == "0.0.1+dependabot"
43
-
44
- dependencies <<
45
- Dependency.new(
46
- name: normalised_name(dep["name"], dep["extras"]),
47
- version: dep["version"]&.include?("*") ? nil : dep["version"],
48
- requirements: [{
49
- requirement: dep["requirement"],
50
- file: Pathname.new(dep["file"]).cleanpath.to_path,
51
- source: nil,
52
- groups: [dep["requirement_type"]]
53
- }],
54
- package_manager: "uv"
55
- )
56
- end
57
- dependencies
58
- end
59
-
60
- private
61
-
62
- sig { returns(T::Array[Dependabot::DependencyFile]) }
63
- attr_reader :dependency_files
64
-
65
- sig { returns(T.untyped) }
66
- def parsed_setup_file
67
- SharedHelpers.in_a_temporary_directory do
68
- write_temporary_dependency_files
69
-
70
- requirements = SharedHelpers.run_helper_subprocess(
71
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
72
- function: "parse_setup",
73
- args: [Dir.pwd]
74
- )
75
-
76
- check_requirements(requirements)
77
- requirements
78
- end
79
- rescue SharedHelpers::HelperSubprocessFailed => e
80
- raise Dependabot::DependencyFileNotEvaluatable, e.message if e.message.start_with?("InstallationError")
81
-
82
- return [] unless setup_file
83
-
84
- parsed_sanitized_setup_file
85
- end
86
-
87
- sig { returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
88
- def parsed_sanitized_setup_file
89
- SharedHelpers.in_a_temporary_directory do
90
- write_sanitized_setup_file
91
-
92
- requirements = SharedHelpers.run_helper_subprocess(
93
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
94
- function: "parse_setup",
95
- args: [Dir.pwd]
96
- )
97
-
98
- check_requirements(requirements)
99
- requirements
100
- end
101
- rescue SharedHelpers::HelperSubprocessFailed
102
- # Assume there are no dependencies in setup.py files that fail to
103
- # parse. This isn't ideal, and we should continue to improve
104
- # parsing, but there are a *lot* of things that can go wrong at
105
- # the moment!
106
- []
107
- end
108
-
109
- sig { params(requirements: T.untyped).returns(T.untyped) }
110
- def check_requirements(requirements)
111
- requirements&.each do |dep|
112
- next unless dep["requirement"]
113
-
114
- Uv::Requirement.new(dep["requirement"].split(","))
115
- rescue Gem::Requirement::BadRequirementError => e
116
- raise Dependabot::DependencyFileNotEvaluatable, e.message
117
- end
118
- end
119
-
120
- sig { void }
121
- def write_temporary_dependency_files
122
- dependency_files
123
- .reject { |f| f.name == ".python-version" }
124
- .each do |file|
125
- path = file.name
126
- FileUtils.mkdir_p(Pathname.new(path).dirname)
127
- File.write(path, file.content)
128
- end
129
- end
130
-
131
- # Write a setup.py with only entries for the requires fields.
132
- #
133
- # This sanitization is far from perfect (it will fail if any of the
134
- # entries are dynamic), but it is an alternative approach to the one
135
- # used in parser.py which sometimes succeeds when that has failed.
136
- sig { void }
137
- def write_sanitized_setup_file
138
- install_requires = get_regexed_req_array(INSTALL_REQUIRES_REGEX)
139
- setup_requires = get_regexed_req_array(SETUP_REQUIRES_REGEX)
140
- tests_require = get_regexed_req_array(TESTS_REQUIRE_REGEX)
141
- extras_require = get_regexed_req_dict(EXTRAS_REQUIRE_REGEX)
142
-
143
- tmp = "from setuptools import setup\n\n" \
144
- "setup(name=\"sanitized-package\",version=\"0.0.1\","
145
-
146
- tmp += "install_requires=#{install_requires}," if install_requires
147
- tmp += "setup_requires=#{setup_requires}," if setup_requires
148
- tmp += "tests_require=#{tests_require}," if tests_require
149
- tmp += "extras_require=#{extras_require}," if extras_require
150
- tmp += ")"
151
-
152
- File.write("setup.py", tmp)
153
- end
154
-
155
- sig { params(regex: Regexp).returns(T.nilable(String)) }
156
- def get_regexed_req_array(regex)
157
- return unless (mch = setup_file.content.match(regex))
158
-
159
- "[#{mch.post_match[0..closing_bracket_index(mch.post_match, '[')]}"
160
- end
161
-
162
- sig { params(regex: Regexp).returns(T.nilable(String)) }
163
- def get_regexed_req_dict(regex)
164
- return unless (mch = setup_file.content.match(regex))
165
-
166
- "{#{mch.post_match[0..closing_bracket_index(mch.post_match, '{')]}"
167
- end
168
-
169
- sig { params(string: String, bracket: String).returns(Integer) }
170
- def closing_bracket_index(string, bracket)
171
- closes_required = 1
172
-
173
- string.chars.each_with_index do |char, index|
174
- closes_required += 1 if char == bracket
175
- closes_required -= 1 if char == CLOSING_BRACKET.fetch(bracket)
176
- return index if closes_required.zero?
177
- end
178
-
179
- 0
180
- end
181
-
182
- sig { params(name: String, extras: T::Array[String]).returns(String) }
183
- def normalised_name(name, extras)
184
- NameNormaliser.normalise_including_extras(name, extras)
185
- end
186
-
187
- sig { returns(T.untyped) }
188
- def setup_file
189
- dependency_files.find { |f| f.name == "setup.py" }
190
- end
191
- end
192
- end
193
- end
194
- end
@@ -1,110 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "dependabot/shared_helpers"
5
- require "dependabot/uv/file_parser"
6
- require "json"
7
- require "sorbet-runtime"
8
-
9
- module Dependabot
10
- module Uv
11
- class PipenvRunner
12
- extend T::Sig
13
-
14
- sig do
15
- params(
16
- dependency: Dependabot::Dependency,
17
- lockfile: T.nilable(Dependabot::DependencyFile),
18
- language_version_manager: LanguageVersionManager
19
- )
20
- .void
21
- end
22
- def initialize(dependency:, lockfile:, language_version_manager:)
23
- @dependency = dependency
24
- @lockfile = lockfile
25
- @language_version_manager = language_version_manager
26
- end
27
-
28
- sig { params(constraint: String).returns(String) }
29
- def run_upgrade(constraint)
30
- constraint = "" if constraint == "*"
31
- command = "pyenv exec pipenv upgrade --verbose #{dependency_name}#{constraint}"
32
- command << " --dev" if lockfile_section == "develop"
33
-
34
- run(command, fingerprint: "pyenv exec pipenv upgrade --verbose <dependency_name><constraint>")
35
- end
36
-
37
- sig { params(constraint: String).returns(T.nilable(String)) }
38
- def run_upgrade_and_fetch_version(constraint)
39
- run_upgrade(constraint)
40
-
41
- updated_lockfile = JSON.parse(File.read("Pipfile.lock"))
42
-
43
- fetch_version_from_parsed_lockfile(updated_lockfile)
44
- end
45
-
46
- sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
47
- def run(command, fingerprint: nil)
48
- run_command(
49
- "pyenv local #{language_version_manager.python_major_minor}",
50
- fingerprint: "pyenv local <python_major_minor>"
51
- )
52
-
53
- run_command(command, fingerprint: fingerprint)
54
- end
55
-
56
- private
57
-
58
- sig { returns(Dependabot::Dependency) }
59
- attr_reader :dependency
60
-
61
- sig { returns(T.nilable(Dependabot::DependencyFile)) }
62
- attr_reader :lockfile
63
-
64
- sig { returns(LanguageVersionManager) }
65
- attr_reader :language_version_manager
66
-
67
- sig { params(updated_lockfile: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
68
- def fetch_version_from_parsed_lockfile(updated_lockfile)
69
- deps = updated_lockfile[lockfile_section] || {}
70
-
71
- deps.dig(dependency_name, "version")
72
- &.gsub(/^==/, "")
73
- end
74
-
75
- sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
76
- def run_command(command, fingerprint: nil)
77
- SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
78
- end
79
-
80
- sig { returns(String) }
81
- def lockfile_section
82
- if dependency.requirements.any?
83
- T.must(dependency.requirements.first)[:groups].first
84
- else
85
- Uv::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
86
- section = keys.fetch(:lockfile)
87
- return section if JSON.parse(T.must(T.must(lockfile).content))[section].keys.any?(dependency_name)
88
- end
89
- end
90
- end
91
-
92
- sig { returns(String) }
93
- def dependency_name
94
- dependency.metadata[:original_name] || dependency.name
95
- end
96
-
97
- sig { returns(T::Hash[String, String]) }
98
- def pipenv_env_variables
99
- {
100
- "PIPENV_YES" => "true", # Install new Python ver if needed
101
- "PIPENV_MAX_RETRIES" => "3", # Retry timeouts
102
- "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
103
- "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
104
- "PIP_DEFAULT_TIMEOUT" => "60", # Set pip timeout to 1 minute
105
- "COLUMNS" => "250" # Avoid line wrapping
106
- }
107
- end
108
- end
109
- end
110
- end