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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +34 -0
  3. data/helpers/lib/__init__.py +0 -0
  4. data/helpers/lib/hasher.py +36 -0
  5. data/helpers/lib/parser.py +270 -0
  6. data/helpers/requirements.txt +13 -0
  7. data/helpers/run.py +22 -0
  8. data/lib/dependabot/uv/authed_url_builder.rb +31 -0
  9. data/lib/dependabot/uv/file_fetcher.rb +328 -0
  10. data/lib/dependabot/uv/file_parser/pipfile_files_parser.rb +192 -0
  11. data/lib/dependabot/uv/file_parser/pyproject_files_parser.rb +345 -0
  12. data/lib/dependabot/uv/file_parser/python_requirement_parser.rb +185 -0
  13. data/lib/dependabot/uv/file_parser/setup_file_parser.rb +193 -0
  14. data/lib/dependabot/uv/file_parser.rb +437 -0
  15. data/lib/dependabot/uv/file_updater/compile_file_updater.rb +576 -0
  16. data/lib/dependabot/uv/file_updater/pyproject_preparer.rb +124 -0
  17. data/lib/dependabot/uv/file_updater/requirement_file_updater.rb +73 -0
  18. data/lib/dependabot/uv/file_updater/requirement_replacer.rb +214 -0
  19. data/lib/dependabot/uv/file_updater.rb +105 -0
  20. data/lib/dependabot/uv/language.rb +76 -0
  21. data/lib/dependabot/uv/language_version_manager.rb +114 -0
  22. data/lib/dependabot/uv/metadata_finder.rb +186 -0
  23. data/lib/dependabot/uv/name_normaliser.rb +26 -0
  24. data/lib/dependabot/uv/native_helpers.rb +38 -0
  25. data/lib/dependabot/uv/package_manager.rb +54 -0
  26. data/lib/dependabot/uv/pip_compile_file_matcher.rb +38 -0
  27. data/lib/dependabot/uv/pipenv_runner.rb +108 -0
  28. data/lib/dependabot/uv/requirement.rb +163 -0
  29. data/lib/dependabot/uv/requirement_parser.rb +60 -0
  30. data/lib/dependabot/uv/update_checker/index_finder.rb +227 -0
  31. data/lib/dependabot/uv/update_checker/latest_version_finder.rb +297 -0
  32. data/lib/dependabot/uv/update_checker/pip_compile_version_resolver.rb +506 -0
  33. data/lib/dependabot/uv/update_checker/pip_version_resolver.rb +73 -0
  34. data/lib/dependabot/uv/update_checker/requirements_updater.rb +391 -0
  35. data/lib/dependabot/uv/update_checker.rb +317 -0
  36. data/lib/dependabot/uv/version.rb +321 -0
  37. data/lib/dependabot/uv.rb +35 -0
  38. 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