dependabot-uv 0.299.1
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 +7 -0
- data/helpers/build +34 -0
- data/helpers/lib/__init__.py +0 -0
- data/helpers/lib/hasher.py +36 -0
- data/helpers/lib/parser.py +270 -0
- data/helpers/requirements.txt +13 -0
- data/helpers/run.py +22 -0
- data/lib/dependabot/uv/authed_url_builder.rb +31 -0
- data/lib/dependabot/uv/file_fetcher.rb +328 -0
- data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
- data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
- data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
- data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
- data/lib/dependabot/uv/file_parser.rb +437 -0
- data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
- data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
- data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
- data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
- data/lib/dependabot/uv/file_updater.rb +105 -0
- data/lib/dependabot/uv/language.rb +76 -0
- data/lib/dependabot/uv/language_version_manager.rb +114 -0
- data/lib/dependabot/uv/metadata_finder.rb +186 -0
- data/lib/dependabot/uv/name_normaliser.rb +26 -0
- data/lib/dependabot/uv/native_helpers.rb +38 -0
- data/lib/dependabot/uv/package_manager.rb +54 -0
- data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
- data/lib/dependabot/uv/pipenv_runner.rb +108 -0
- data/lib/dependabot/uv/requirement.rb +163 -0
- data/lib/dependabot/uv/requirement_parser.rb +60 -0
- data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
- data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
- data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
- data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
- data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
- data/lib/dependabot/uv/update_checker.rb +317 -0
- data/lib/dependabot/uv/version.rb +321 -0
- data/lib/dependabot/uv.rb +35 -0
- metadata +306 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "excon"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
require "dependabot/metadata_finders"
|
8
|
+
require "dependabot/metadata_finders/base"
|
9
|
+
require "dependabot/registry_client"
|
10
|
+
require "dependabot/uv/authed_url_builder"
|
11
|
+
require "dependabot/uv/name_normaliser"
|
12
|
+
|
13
|
+
module Dependabot
|
14
|
+
module Uv
|
15
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
16
|
+
MAIN_PYPI_URL = "https://pypi.org/pypi"
|
17
|
+
|
18
|
+
def homepage_url
|
19
|
+
pypi_listing.dig("info", "home_page") ||
|
20
|
+
pypi_listing.dig("info", "project_urls", "Homepage") ||
|
21
|
+
pypi_listing.dig("info", "project_urls", "homepage") ||
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def look_up_source
|
28
|
+
potential_source_urls = [
|
29
|
+
pypi_listing.dig("info", "project_urls", "Source"),
|
30
|
+
pypi_listing.dig("info", "project_urls", "Repository"),
|
31
|
+
pypi_listing.dig("info", "home_page"),
|
32
|
+
pypi_listing.dig("info", "download_url"),
|
33
|
+
pypi_listing.dig("info", "docs_url")
|
34
|
+
].compact
|
35
|
+
|
36
|
+
potential_source_urls +=
|
37
|
+
(pypi_listing.dig("info", "project_urls") || {}).values
|
38
|
+
|
39
|
+
source_url = potential_source_urls.find { |url| Source.from_url(url) }
|
40
|
+
source_url ||= source_from_description
|
41
|
+
source_url ||= source_from_homepage
|
42
|
+
|
43
|
+
Source.from_url(source_url)
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
47
|
+
def source_from_description
|
48
|
+
potential_source_urls = []
|
49
|
+
desc = pypi_listing.dig("info", "description")
|
50
|
+
return unless desc
|
51
|
+
|
52
|
+
desc.scan(Source::SOURCE_REGEX) do
|
53
|
+
potential_source_urls << Regexp.last_match.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
# Looking for a source where the repo name exactly matches the
|
57
|
+
# dependency name
|
58
|
+
match_url = potential_source_urls.find do |url|
|
59
|
+
repo = Source.from_url(url)&.repo
|
60
|
+
repo&.downcase&.end_with?(normalised_dependency_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
return match_url if match_url
|
64
|
+
|
65
|
+
# Failing that, look for a source where the full dependency name is
|
66
|
+
# mentioned when the link is followed
|
67
|
+
@source_from_description ||=
|
68
|
+
potential_source_urls.find do |url|
|
69
|
+
full_url = Source.from_url(url)&.url
|
70
|
+
next unless full_url
|
71
|
+
|
72
|
+
response = Dependabot::RegistryClient.get(url: full_url)
|
73
|
+
next unless response.status == 200
|
74
|
+
|
75
|
+
response.body.include?(normalised_dependency_name)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
79
|
+
|
80
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
81
|
+
def source_from_homepage
|
82
|
+
return unless homepage_body
|
83
|
+
|
84
|
+
potential_source_urls = []
|
85
|
+
homepage_body.scan(Source::SOURCE_REGEX) do
|
86
|
+
potential_source_urls << Regexp.last_match.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
match_url = potential_source_urls.find do |url|
|
90
|
+
repo = Source.from_url(url)&.repo
|
91
|
+
repo&.downcase&.end_with?(normalised_dependency_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
return match_url if match_url
|
95
|
+
|
96
|
+
@source_from_homepage ||=
|
97
|
+
potential_source_urls.find do |url|
|
98
|
+
full_url = Source.from_url(url)&.url
|
99
|
+
next unless full_url
|
100
|
+
|
101
|
+
response = Dependabot::RegistryClient.get(url: full_url)
|
102
|
+
next unless response.status == 200
|
103
|
+
|
104
|
+
response.body.include?(normalised_dependency_name)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
108
|
+
|
109
|
+
def homepage_body
|
110
|
+
homepage_url = pypi_listing.dig("info", "home_page")
|
111
|
+
|
112
|
+
return unless homepage_url
|
113
|
+
return if [
|
114
|
+
"pypi.org",
|
115
|
+
"pypi.python.org"
|
116
|
+
].include?(URI(homepage_url).host)
|
117
|
+
|
118
|
+
@homepage_response ||=
|
119
|
+
begin
|
120
|
+
Dependabot::RegistryClient.get(url: homepage_url)
|
121
|
+
rescue Excon::Error::Timeout, Excon::Error::Socket,
|
122
|
+
Excon::Error::TooManyRedirects, ArgumentError
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
return unless @homepage_response&.status == 200
|
127
|
+
|
128
|
+
@homepage_response.body
|
129
|
+
end
|
130
|
+
|
131
|
+
def pypi_listing
|
132
|
+
return @pypi_listing unless @pypi_listing.nil?
|
133
|
+
return @pypi_listing = {} if dependency.version&.include?("+")
|
134
|
+
|
135
|
+
possible_listing_urls.each do |url|
|
136
|
+
response = fetch_authed_url(url)
|
137
|
+
next unless response.status == 200
|
138
|
+
|
139
|
+
@pypi_listing = JSON.parse(response.body)
|
140
|
+
return @pypi_listing
|
141
|
+
rescue JSON::ParserError
|
142
|
+
next
|
143
|
+
rescue Excon::Error::Timeout
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
@pypi_listing = {} # No listing found
|
148
|
+
end
|
149
|
+
|
150
|
+
def fetch_authed_url(url)
|
151
|
+
if url.match(%r{(.*)://(.*?):(.*)@([^@]+)$}) &&
|
152
|
+
Regexp.last_match&.captures&.[](1)&.include?("@")
|
153
|
+
protocol, user, pass, url = T.must(Regexp.last_match).captures
|
154
|
+
|
155
|
+
Dependabot::RegistryClient.get(
|
156
|
+
url: "#{protocol}://#{url}",
|
157
|
+
options: {
|
158
|
+
user: user,
|
159
|
+
password: pass
|
160
|
+
}
|
161
|
+
)
|
162
|
+
else
|
163
|
+
Dependabot::RegistryClient.get(url: url)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def possible_listing_urls
|
168
|
+
credential_urls =
|
169
|
+
credentials
|
170
|
+
.select { |cred| cred["type"] == "python_index" }
|
171
|
+
.map { |c| AuthedUrlBuilder.authed_url(credential: c) }
|
172
|
+
|
173
|
+
(credential_urls + [MAIN_PYPI_URL]).map do |base_url|
|
174
|
+
base_url.gsub(%r{/$}, "") + "/#{normalised_dependency_name}/json"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Strip [extras] from name (dependency_name[extra_dep,other_extra])
|
179
|
+
def normalised_dependency_name
|
180
|
+
NameNormaliser.normalise(dependency.name)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
Dependabot::MetadataFinders.register("uv", Dependabot::Uv::MetadataFinder)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
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
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Dependabot
|
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
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
require "dependabot/uv/version"
|
6
|
+
require "dependabot/ecosystem"
|
7
|
+
require "dependabot/uv/requirement"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Uv
|
11
|
+
ECOSYSTEM = "uv"
|
12
|
+
|
13
|
+
SUPPORTED_PYTHON_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
14
|
+
|
15
|
+
DEPRECATED_PYTHON_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
16
|
+
|
17
|
+
class PackageManager < Dependabot::Ecosystem::VersionManager
|
18
|
+
extend T::Sig
|
19
|
+
|
20
|
+
NAME = "uv"
|
21
|
+
MANIFEST_FILENAME = ".in"
|
22
|
+
|
23
|
+
SUPPORTED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
24
|
+
|
25
|
+
DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
|
26
|
+
|
27
|
+
sig do
|
28
|
+
params(
|
29
|
+
raw_version: String,
|
30
|
+
requirement: T.nilable(Requirement)
|
31
|
+
).void
|
32
|
+
end
|
33
|
+
def initialize(raw_version, requirement = nil)
|
34
|
+
super(
|
35
|
+
name: NAME,
|
36
|
+
version: Version.new(raw_version),
|
37
|
+
deprecated_versions: DEPRECATED_VERSIONS,
|
38
|
+
supported_versions: SUPPORTED_VERSIONS,
|
39
|
+
requirement: requirement,
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { override.returns(T::Boolean) }
|
44
|
+
def deprecated?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { override.returns(T::Boolean) }
|
49
|
+
def unsupported?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Dependabot
|
5
|
+
module Uv
|
6
|
+
class PipCompileFileMatcher
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(requirements_in_files: T::Array[Dependabot::Uv::Requirement]).void }
|
10
|
+
def initialize(requirements_in_files)
|
11
|
+
@requirements_in_files = requirements_in_files
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) }
|
15
|
+
def lockfile_for_pip_compile_file?(file)
|
16
|
+
return false unless requirements_in_files.any?
|
17
|
+
|
18
|
+
name = file.name
|
19
|
+
return false unless name.end_with?(".txt")
|
20
|
+
|
21
|
+
return true if file.content&.match?(output_file_regex(name))
|
22
|
+
|
23
|
+
basename = name.gsub(/\.txt$/, "")
|
24
|
+
requirements_in_files.any? { |f| f.instance_variable_get(:@name) == basename + ".in" }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { returns(T::Array[Dependabot::Uv::Requirement]) }
|
30
|
+
attr_reader :requirements_in_files
|
31
|
+
|
32
|
+
sig { params(filename: T.any(String, Symbol)).returns(String) }
|
33
|
+
def output_file_regex(filename)
|
34
|
+
"--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,108 @@
|
|
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
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
61
|
+
attr_reader :lockfile
|
62
|
+
sig { returns(LanguageVersionManager) }
|
63
|
+
attr_reader :language_version_manager
|
64
|
+
|
65
|
+
sig { params(updated_lockfile: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
|
66
|
+
def fetch_version_from_parsed_lockfile(updated_lockfile)
|
67
|
+
deps = updated_lockfile[lockfile_section] || {}
|
68
|
+
|
69
|
+
deps.dig(dependency_name, "version")
|
70
|
+
&.gsub(/^==/, "")
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
|
74
|
+
def run_command(command, fingerprint: nil)
|
75
|
+
SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(String) }
|
79
|
+
def lockfile_section
|
80
|
+
if dependency.requirements.any?
|
81
|
+
T.must(dependency.requirements.first)[:groups].first
|
82
|
+
else
|
83
|
+
Uv::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
|
84
|
+
section = keys.fetch(:lockfile)
|
85
|
+
return section if JSON.parse(T.must(T.must(lockfile).content))[section].keys.any?(dependency_name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { returns(String) }
|
91
|
+
def dependency_name
|
92
|
+
dependency.metadata[:original_name] || dependency.name
|
93
|
+
end
|
94
|
+
|
95
|
+
sig { returns(T::Hash[String, String]) }
|
96
|
+
def pipenv_env_variables
|
97
|
+
{
|
98
|
+
"PIPENV_YES" => "true", # Install new Python ver if needed
|
99
|
+
"PIPENV_MAX_RETRIES" => "3", # Retry timeouts
|
100
|
+
"PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
|
101
|
+
"PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
|
102
|
+
"PIP_DEFAULT_TIMEOUT" => "60", # Set pip timeout to 1 minute
|
103
|
+
"COLUMNS" => "250" # Avoid line wrapping
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
require "dependabot/requirement"
|
7
|
+
require "dependabot/utils"
|
8
|
+
require "dependabot/uv/version"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Uv
|
12
|
+
class Requirement < Dependabot::Requirement
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
OR_SEPARATOR = /(?<=[a-zA-Z0-9)*])\s*\|+/
|
16
|
+
|
17
|
+
# Add equality and arbitrary-equality matchers
|
18
|
+
OPS = OPS.merge(
|
19
|
+
"==" => ->(v, r) { v == r },
|
20
|
+
"===" => ->(v, r) { v.to_s == r.to_s }
|
21
|
+
)
|
22
|
+
|
23
|
+
quoted = OPS.keys.sort_by(&:length).reverse
|
24
|
+
.map { |k| Regexp.quote(k) }.join("|")
|
25
|
+
version_pattern = Uv::Version::VERSION_PATTERN
|
26
|
+
|
27
|
+
PATTERN_RAW = "\\s*(?<op>#{quoted})?\\s*(?<version>#{version_pattern})\\s*".freeze
|
28
|
+
PATTERN = /\A#{PATTERN_RAW}\z/
|
29
|
+
PARENS_PATTERN = /\A\(([^)]+)\)\z/
|
30
|
+
|
31
|
+
def self.parse(obj)
|
32
|
+
return ["=", Uv::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
|
33
|
+
|
34
|
+
line = obj.to_s
|
35
|
+
if (matches = PARENS_PATTERN.match(line))
|
36
|
+
line = matches[1]
|
37
|
+
end
|
38
|
+
|
39
|
+
unless (matches = PATTERN.match(line))
|
40
|
+
msg = "Illformed requirement [#{obj.inspect}]"
|
41
|
+
raise BadRequirementError, msg
|
42
|
+
end
|
43
|
+
|
44
|
+
return DefaultRequirement if matches[:op] == ">=" && matches[:version] == "0"
|
45
|
+
|
46
|
+
[matches[:op] || "=", Uv::Version.new(T.must(matches[:version]))]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of requirements. At least one requirement from the
|
50
|
+
# returned array must be satisfied for a version to be valid.
|
51
|
+
#
|
52
|
+
# NOTE: Or requirements are only valid for Poetry.
|
53
|
+
sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
|
54
|
+
def self.requirements_array(requirement_string)
|
55
|
+
return [new(nil)] if requirement_string.nil?
|
56
|
+
|
57
|
+
if (matches = PARENS_PATTERN.match(requirement_string))
|
58
|
+
requirement_string = matches[1]
|
59
|
+
end
|
60
|
+
|
61
|
+
T.must(requirement_string).strip.split(OR_SEPARATOR).map do |req_string|
|
62
|
+
new(req_string.strip)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize(*requirements)
|
67
|
+
requirements = requirements.flatten.flat_map do |req_string|
|
68
|
+
next if req_string.nil?
|
69
|
+
|
70
|
+
# Standard python doesn't support whitespace in requirements, but Poetry does.
|
71
|
+
req_string = req_string.gsub(/(\d +)([<=>])/, '\1,\2')
|
72
|
+
|
73
|
+
req_string.split(",").map(&:strip).map do |r|
|
74
|
+
convert_python_constraint_to_ruby_constraint(r)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
super(requirements)
|
79
|
+
end
|
80
|
+
|
81
|
+
def satisfied_by?(version)
|
82
|
+
version = Uv::Version.new(version.to_s)
|
83
|
+
|
84
|
+
requirements.all? { |op, rv| (OPS[op] || OPS["="]).call(version, rv) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def exact?
|
88
|
+
return false unless @requirements.size == 1
|
89
|
+
|
90
|
+
%w(= == ===).include?(@requirements[0][0])
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def convert_python_constraint_to_ruby_constraint(req_string)
|
96
|
+
return nil if req_string.nil?
|
97
|
+
return nil if req_string == "*"
|
98
|
+
|
99
|
+
req_string = req_string.gsub("~=", "~>")
|
100
|
+
req_string = req_string.gsub(/(?<=\d)[<=>].*\Z/, "")
|
101
|
+
|
102
|
+
if req_string.match?(/~[^>]/) then convert_tilde_req(req_string)
|
103
|
+
elsif req_string.start_with?("^") then convert_caret_req(req_string)
|
104
|
+
elsif req_string.include?(".*") then convert_wildcard(req_string)
|
105
|
+
else
|
106
|
+
req_string
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Poetry uses ~ requirements.
|
111
|
+
# https://github.com/sdispater/poetry#tilde-requirements
|
112
|
+
def convert_tilde_req(req_string)
|
113
|
+
version = req_string.gsub(/^~\>?/, "")
|
114
|
+
parts = version.split(".")
|
115
|
+
parts << "0" if parts.count < 3
|
116
|
+
"~> #{parts.join('.')}"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Poetry uses ^ requirements
|
120
|
+
# https://github.com/sdispater/poetry#caret-requirement
|
121
|
+
def convert_caret_req(req_string)
|
122
|
+
version = req_string.gsub(/^\^/, "")
|
123
|
+
parts = version.split(".")
|
124
|
+
parts.fill(0, parts.length...3)
|
125
|
+
first_non_zero = parts.find { |d| d != "0" }
|
126
|
+
first_non_zero_index =
|
127
|
+
first_non_zero ? parts.index(first_non_zero) : parts.count - 1
|
128
|
+
upper_bound = parts.map.with_index do |part, i|
|
129
|
+
if i < first_non_zero_index then part
|
130
|
+
elsif i == first_non_zero_index then (part.to_i + 1).to_s
|
131
|
+
# .dev has lowest precedence: https://packaging.python.org/en/latest/specifications/version-specifiers/#summary-of-permitted-suffixes-and-relative-ordering
|
132
|
+
elsif i > first_non_zero_index && i == 2 then "0.dev"
|
133
|
+
else
|
134
|
+
0
|
135
|
+
end
|
136
|
+
end.join(".")
|
137
|
+
|
138
|
+
[">= #{version}", "< #{upper_bound}"]
|
139
|
+
end
|
140
|
+
|
141
|
+
def convert_wildcard(req_string)
|
142
|
+
# NOTE: This isn't perfect. It replaces the "!= 1.0.*" case with
|
143
|
+
# "!= 1.0.0". There's no way to model this correctly in Ruby :'(
|
144
|
+
quoted_ops = OPS.keys.sort_by(&:length).reverse
|
145
|
+
.map { |k| Regexp.quote(k) }.join("|")
|
146
|
+
op = req_string.match(/\A\s*(#{quoted_ops})?/)
|
147
|
+
.captures.first.to_s&.strip
|
148
|
+
exact_op = ["", "=", "==", "==="].include?(op)
|
149
|
+
|
150
|
+
req_string.strip
|
151
|
+
.split(".")
|
152
|
+
.first(req_string.split(".").index { |s| s.include?("*") } + 1)
|
153
|
+
.join(".")
|
154
|
+
.gsub(/\*(?!$)/, "0")
|
155
|
+
.gsub(/\*$/, "0.dev")
|
156
|
+
.tap { |s| exact_op ? s.gsub!(/^(?<!!)=*/, "~>") : s }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
Dependabot::Utils
|
163
|
+
.register_requirement_class("uv", Dependabot::Uv::Requirement)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Dependabot
|
5
|
+
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
|
59
|
+
end
|
60
|
+
end
|