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,193 @@
|
|
|
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
|
+
INSTALL_REQUIRES_REGEX = /install_requires\s*=\s*\[/m
|
|
19
|
+
SETUP_REQUIRES_REGEX = /setup_requires\s*=\s*\[/m
|
|
20
|
+
TESTS_REQUIRE_REGEX = /tests_require\s*=\s*\[/m
|
|
21
|
+
EXTRAS_REQUIRE_REGEX = /extras_require\s*=\s*\{/m
|
|
22
|
+
|
|
23
|
+
CLOSING_BRACKET = T.let({ "[" => "]", "{" => "}" }.freeze, T.any(T.untyped, T.untyped))
|
|
24
|
+
|
|
25
|
+
sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
|
|
26
|
+
def initialize(dependency_files:)
|
|
27
|
+
@dependency_files = dependency_files
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
31
|
+
def dependency_set
|
|
32
|
+
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
|
33
|
+
|
|
34
|
+
parsed_setup_file.each do |dep|
|
|
35
|
+
# If a requirement has a `<` or `<=` marker then updating it is
|
|
36
|
+
# probably blocked. Ignore it.
|
|
37
|
+
next if dep["markers"].include?("<")
|
|
38
|
+
|
|
39
|
+
# If the requirement is our inserted version, ignore it
|
|
40
|
+
# (we wouldn't be able to update it)
|
|
41
|
+
next if dep["version"] == "0.0.1+dependabot"
|
|
42
|
+
|
|
43
|
+
dependencies <<
|
|
44
|
+
Dependency.new(
|
|
45
|
+
name: normalised_name(dep["name"], dep["extras"]),
|
|
46
|
+
version: dep["version"]&.include?("*") ? nil : dep["version"],
|
|
47
|
+
requirements: [{
|
|
48
|
+
requirement: dep["requirement"],
|
|
49
|
+
file: Pathname.new(dep["file"]).cleanpath.to_path,
|
|
50
|
+
source: nil,
|
|
51
|
+
groups: [dep["requirement_type"]]
|
|
52
|
+
}],
|
|
53
|
+
package_manager: "uv"
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
dependencies
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
62
|
+
attr_reader :dependency_files
|
|
63
|
+
|
|
64
|
+
sig { returns(T.untyped) }
|
|
65
|
+
def parsed_setup_file
|
|
66
|
+
SharedHelpers.in_a_temporary_directory do
|
|
67
|
+
write_temporary_dependency_files
|
|
68
|
+
|
|
69
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
|
70
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
|
71
|
+
function: "parse_setup",
|
|
72
|
+
args: [Dir.pwd]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
check_requirements(requirements)
|
|
76
|
+
requirements
|
|
77
|
+
end
|
|
78
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
79
|
+
raise Dependabot::DependencyFileNotEvaluatable, e.message if e.message.start_with?("InstallationError")
|
|
80
|
+
|
|
81
|
+
return [] unless setup_file
|
|
82
|
+
|
|
83
|
+
parsed_sanitized_setup_file
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
|
|
87
|
+
def parsed_sanitized_setup_file
|
|
88
|
+
SharedHelpers.in_a_temporary_directory do
|
|
89
|
+
write_sanitized_setup_file
|
|
90
|
+
|
|
91
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
|
92
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
|
93
|
+
function: "parse_setup",
|
|
94
|
+
args: [Dir.pwd]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
check_requirements(requirements)
|
|
98
|
+
requirements
|
|
99
|
+
end
|
|
100
|
+
rescue SharedHelpers::HelperSubprocessFailed
|
|
101
|
+
# Assume there are no dependencies in setup.py files that fail to
|
|
102
|
+
# parse. This isn't ideal, and we should continue to improve
|
|
103
|
+
# parsing, but there are a *lot* of things that can go wrong at
|
|
104
|
+
# the moment!
|
|
105
|
+
[]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(requirements: T.untyped).returns(T.untyped) }
|
|
109
|
+
def check_requirements(requirements)
|
|
110
|
+
requirements&.each do |dep|
|
|
111
|
+
next unless dep["requirement"]
|
|
112
|
+
|
|
113
|
+
Uv::Requirement.new(dep["requirement"].split(","))
|
|
114
|
+
rescue Gem::Requirement::BadRequirementError => e
|
|
115
|
+
raise Dependabot::DependencyFileNotEvaluatable, e.message
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { void }
|
|
120
|
+
def write_temporary_dependency_files
|
|
121
|
+
dependency_files
|
|
122
|
+
.reject { |f| f.name == ".python-version" }
|
|
123
|
+
.each do |file|
|
|
124
|
+
path = file.name
|
|
125
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
|
126
|
+
File.write(path, file.content)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Write a setup.py with only entries for the requires fields.
|
|
131
|
+
#
|
|
132
|
+
# This sanitization is far from perfect (it will fail if any of the
|
|
133
|
+
# entries are dynamic), but it is an alternative approach to the one
|
|
134
|
+
# used in parser.py which sometimes succeeds when that has failed.
|
|
135
|
+
sig { void }
|
|
136
|
+
def write_sanitized_setup_file
|
|
137
|
+
install_requires = get_regexed_req_array(INSTALL_REQUIRES_REGEX)
|
|
138
|
+
setup_requires = get_regexed_req_array(SETUP_REQUIRES_REGEX)
|
|
139
|
+
tests_require = get_regexed_req_array(TESTS_REQUIRE_REGEX)
|
|
140
|
+
extras_require = get_regexed_req_dict(EXTRAS_REQUIRE_REGEX)
|
|
141
|
+
|
|
142
|
+
tmp = "from setuptools import setup\n\n" \
|
|
143
|
+
"setup(name=\"sanitized-package\",version=\"0.0.1\","
|
|
144
|
+
|
|
145
|
+
tmp += "install_requires=#{install_requires}," if install_requires
|
|
146
|
+
tmp += "setup_requires=#{setup_requires}," if setup_requires
|
|
147
|
+
tmp += "tests_require=#{tests_require}," if tests_require
|
|
148
|
+
tmp += "extras_require=#{extras_require}," if extras_require
|
|
149
|
+
tmp += ")"
|
|
150
|
+
|
|
151
|
+
File.write("setup.py", tmp)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { params(regex: Regexp).returns(T.nilable(String)) }
|
|
155
|
+
def get_regexed_req_array(regex)
|
|
156
|
+
return unless (mch = setup_file.content.match(regex))
|
|
157
|
+
|
|
158
|
+
"[#{mch.post_match[0..closing_bracket_index(mch.post_match, '[')]}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
sig { params(regex: Regexp).returns(T.nilable(String)) }
|
|
162
|
+
def get_regexed_req_dict(regex)
|
|
163
|
+
return unless (mch = setup_file.content.match(regex))
|
|
164
|
+
|
|
165
|
+
"{#{mch.post_match[0..closing_bracket_index(mch.post_match, '{')]}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { params(string: String, bracket: String).returns(Integer) }
|
|
169
|
+
def closing_bracket_index(string, bracket)
|
|
170
|
+
closes_required = 1
|
|
171
|
+
|
|
172
|
+
string.chars.each_with_index do |char, index|
|
|
173
|
+
closes_required += 1 if char == bracket
|
|
174
|
+
closes_required -= 1 if char == CLOSING_BRACKET.fetch(bracket)
|
|
175
|
+
return index if closes_required.zero?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { params(name: String, extras: T::Array[String]).returns(String) }
|
|
182
|
+
def normalised_name(name, extras)
|
|
183
|
+
NameNormaliser.normalise_including_extras(name, extras)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sig { returns(T.untyped) }
|
|
187
|
+
def setup_file
|
|
188
|
+
dependency_files.find { |f| f.name == "setup.py" }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "dependabot/dependency"
|
|
5
|
+
require "dependabot/file_parsers"
|
|
6
|
+
require "dependabot/file_parsers/base"
|
|
7
|
+
require "dependabot/file_parsers/base/dependency_set"
|
|
8
|
+
require "dependabot/shared_helpers"
|
|
9
|
+
require "dependabot/uv/requirement"
|
|
10
|
+
require "dependabot/errors"
|
|
11
|
+
require "dependabot/uv/language"
|
|
12
|
+
require "dependabot/uv/native_helpers"
|
|
13
|
+
require "dependabot/uv/name_normaliser"
|
|
14
|
+
require "dependabot/uv/pip_compile_file_matcher"
|
|
15
|
+
require "dependabot/uv/language_version_manager"
|
|
16
|
+
require "dependabot/uv/package_manager"
|
|
17
|
+
|
|
18
|
+
module Dependabot
|
|
19
|
+
module Uv
|
|
20
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
21
|
+
extend T::Sig
|
|
22
|
+
require_relative "file_parser/pipfile_files_parser"
|
|
23
|
+
require_relative "file_parser/pyproject_files_parser"
|
|
24
|
+
require_relative "file_parser/setup_file_parser"
|
|
25
|
+
require_relative "file_parser/python_requirement_parser"
|
|
26
|
+
|
|
27
|
+
DEPENDENCY_GROUP_KEYS = T.let([
|
|
28
|
+
{
|
|
29
|
+
pipfile: "packages",
|
|
30
|
+
lockfile: "default"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pipfile: "dev-packages",
|
|
34
|
+
lockfile: "develop"
|
|
35
|
+
}
|
|
36
|
+
].freeze, T::Array[T::Hash[Symbol, String]])
|
|
37
|
+
REQUIREMENT_FILE_EVALUATION_ERRORS = %w(
|
|
38
|
+
InstallationError RequirementsFileParseError InvalidMarker
|
|
39
|
+
InvalidRequirement ValueError RecursionError
|
|
40
|
+
).freeze
|
|
41
|
+
|
|
42
|
+
# we use this placeholder version in case we are not able to detect any
|
|
43
|
+
# PIP version from shell, we are ensuring that the actual update is not blocked
|
|
44
|
+
# in any way if any metric collection exception start happening
|
|
45
|
+
UNDETECTED_PACKAGE_MANAGER_VERSION = "0.0"
|
|
46
|
+
|
|
47
|
+
sig { override.returns(T::Array[Dependabot::Dependency]) }
|
|
48
|
+
def parse
|
|
49
|
+
# TODO: setup.py from external dependencies is evaluated. Provide guards before removing this.
|
|
50
|
+
raise Dependabot::UnexpectedExternalCode if @reject_external_code
|
|
51
|
+
|
|
52
|
+
dependency_set = DependencySet.new
|
|
53
|
+
|
|
54
|
+
dependency_set += pyproject_file_dependencies if pyproject
|
|
55
|
+
dependency_set += requirement_dependencies if requirement_files.any?
|
|
56
|
+
|
|
57
|
+
dependency_set.dependencies
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { override.returns(Ecosystem) }
|
|
61
|
+
def ecosystem
|
|
62
|
+
@ecosystem ||= T.let(
|
|
63
|
+
Ecosystem.new(
|
|
64
|
+
name: ECOSYSTEM,
|
|
65
|
+
package_manager: package_manager,
|
|
66
|
+
language: language
|
|
67
|
+
),
|
|
68
|
+
T.nilable(Ecosystem)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
sig { returns(Dependabot::Uv::LanguageVersionManager) }
|
|
75
|
+
def language_version_manager
|
|
76
|
+
@language_version_manager ||= T.let(LanguageVersionManager.new(python_requirement_parser:
|
|
77
|
+
python_requirement_parser), T.nilable(LanguageVersionManager))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { returns(Dependabot::Uv::FileParser::PythonRequirementParser) }
|
|
81
|
+
def python_requirement_parser
|
|
82
|
+
@python_requirement_parser ||= T.let(FileParser::PythonRequirementParser.new(dependency_files:
|
|
83
|
+
dependency_files), T.nilable(FileParser::PythonRequirementParser))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { returns(Ecosystem::VersionManager) }
|
|
87
|
+
def package_manager
|
|
88
|
+
if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
|
|
89
|
+
Dependabot.logger.info("Detected package manager : #{detected_package_manager.name}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@package_manager ||= T.let(detected_package_manager, T.nilable(Dependabot::Ecosystem::VersionManager))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { returns(Ecosystem::VersionManager) }
|
|
96
|
+
def detected_package_manager
|
|
97
|
+
setup_python_environment if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
|
|
98
|
+
|
|
99
|
+
PackageManager.new(T.must(detect_pipcompile_version))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Detects the version of pip-compile. If the version cannot be detected, it returns nil
|
|
103
|
+
sig { returns(T.nilable(String)) }
|
|
104
|
+
def detect_pipcompile_version
|
|
105
|
+
if pipcompile_in_file
|
|
106
|
+
package_manager = PackageManager::NAME
|
|
107
|
+
|
|
108
|
+
version = package_manager_version(package_manager)
|
|
109
|
+
.to_s.split("version ").last&.split(")")&.first
|
|
110
|
+
|
|
111
|
+
log_if_version_malformed(package_manager, version)
|
|
112
|
+
|
|
113
|
+
# makes sure we have correct version format returned
|
|
114
|
+
version if version&.match?(/^\d+(?:\.\d+)*$/)
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { params(package_manager: String).returns(T.any(String, T.untyped)) }
|
|
121
|
+
def package_manager_version(package_manager)
|
|
122
|
+
version_info = SharedHelpers.run_shell_command("pyenv exec #{package_manager} --version")
|
|
123
|
+
Dependabot.logger.info("Package manager #{package_manager}, Info : #{version_info}")
|
|
124
|
+
|
|
125
|
+
version_info.match(/\d+(?:\.\d+)*/)&.to_s
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
Dependabot.logger.error(e.message)
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# setup python local setup on file parser stage
|
|
132
|
+
sig { returns(T.nilable(String)) }
|
|
133
|
+
def setup_python_environment
|
|
134
|
+
language_version_manager.install_required_python
|
|
135
|
+
|
|
136
|
+
SharedHelpers.run_shell_command("pyenv local #{language_version_manager.python_major_minor}")
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
Dependabot.logger.error(e.message)
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { params(package_manager: String, version: String).returns(T::Boolean) }
|
|
143
|
+
def log_if_version_malformed(package_manager, version)
|
|
144
|
+
# logs warning if malformed version is found
|
|
145
|
+
if version.match?(/^\d+(?:\.\d+)*$/)
|
|
146
|
+
true
|
|
147
|
+
else
|
|
148
|
+
Dependabot.logger.warn("Detected #{package_manager} with malformed version #{version}")
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
sig { returns(String) }
|
|
154
|
+
def python_raw_version
|
|
155
|
+
if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
|
|
156
|
+
Dependabot.logger.info("Detected python version: #{language_version_manager.python_version}")
|
|
157
|
+
Dependabot.logger.info("Detected python major minor version: #{language_version_manager.python_major_minor}")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
language_version_manager.python_version
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
sig { returns(String) }
|
|
164
|
+
def python_command_version
|
|
165
|
+
language_version_manager.installed_version
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { returns(T.nilable(Ecosystem::VersionManager)) }
|
|
169
|
+
def language
|
|
170
|
+
Language.new(
|
|
171
|
+
detected_version: python_raw_version,
|
|
172
|
+
raw_version: python_command_version
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
177
|
+
def requirement_files
|
|
178
|
+
dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
sig { returns(DependencySet) }
|
|
182
|
+
def pipenv_dependencies
|
|
183
|
+
@pipenv_dependencies ||= T.let(PipfileFilesParser.new(dependency_files:
|
|
184
|
+
dependency_files).dependency_set, T.nilable(DependencySet))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sig { returns(DependencySet) }
|
|
188
|
+
def pyproject_file_dependencies
|
|
189
|
+
@pyproject_file_dependencies ||= T.let(PyprojectFilesParser.new(dependency_files:
|
|
190
|
+
dependency_files).dependency_set, T.nilable(DependencySet))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sig { returns(DependencySet) }
|
|
194
|
+
def requirement_dependencies
|
|
195
|
+
dependencies = DependencySet.new
|
|
196
|
+
parsed_requirement_files.each do |dep|
|
|
197
|
+
# If a requirement has a `<`, `<=` or '==' marker then updating it is
|
|
198
|
+
# probably blocked. Ignore it.
|
|
199
|
+
next if blocking_marker?(dep)
|
|
200
|
+
|
|
201
|
+
name = dep["name"]
|
|
202
|
+
file = dep["file"]
|
|
203
|
+
version = dep["version"]
|
|
204
|
+
original_file = get_original_file(file)
|
|
205
|
+
|
|
206
|
+
requirements =
|
|
207
|
+
if original_file && pip_compile_file_matcher.lockfile_for_pip_compile_file?(original_file) then []
|
|
208
|
+
else
|
|
209
|
+
[{
|
|
210
|
+
requirement: dep["requirement"],
|
|
211
|
+
file: Pathname.new(file).cleanpath.to_path,
|
|
212
|
+
source: nil,
|
|
213
|
+
groups: group_from_filename(file)
|
|
214
|
+
}]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# PyYAML < 6.0 will cause `pip-compile` to fail due to incompatibility with Cython 3. Workaround it.
|
|
218
|
+
SharedHelpers.run_shell_command("pyenv exec pip install cython<3.0") if old_pyyaml?(name, version)
|
|
219
|
+
|
|
220
|
+
dependencies <<
|
|
221
|
+
Dependency.new(
|
|
222
|
+
name: normalised_name(name, dep["extras"]),
|
|
223
|
+
version: version&.include?("*") ? nil : version,
|
|
224
|
+
requirements: requirements,
|
|
225
|
+
package_manager: "uv"
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
dependencies
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
sig { params(name: T.nilable(String), version: T.nilable(String)).returns(T::Boolean) }
|
|
232
|
+
def old_pyyaml?(name, version)
|
|
233
|
+
major_version = version&.split(".")&.first
|
|
234
|
+
return false unless major_version
|
|
235
|
+
|
|
236
|
+
name == "pyyaml" && major_version < "6"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { params(filename: String).returns(T::Array[String]) }
|
|
240
|
+
def group_from_filename(filename)
|
|
241
|
+
if filename.include?("dev") then ["dev-dependencies"]
|
|
242
|
+
else
|
|
243
|
+
["dependencies"]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
sig { params(dep: T.untyped).returns(T::Boolean) }
|
|
248
|
+
def blocking_marker?(dep)
|
|
249
|
+
return false if dep["markers"] == "None"
|
|
250
|
+
|
|
251
|
+
marker = dep["markers"]
|
|
252
|
+
version = python_raw_version
|
|
253
|
+
|
|
254
|
+
if marker.include?("python_version")
|
|
255
|
+
!marker_satisfied?(marker, version)
|
|
256
|
+
else
|
|
257
|
+
return true if dep["markers"].include?("<")
|
|
258
|
+
return false if dep["markers"].include?(">")
|
|
259
|
+
return false if dep["requirement"].nil?
|
|
260
|
+
|
|
261
|
+
dep["requirement"].include?("<")
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
sig do
|
|
266
|
+
params(marker: T.untyped, python_version: T.any(String, Integer, Gem::Version)).returns(T::Boolean)
|
|
267
|
+
end
|
|
268
|
+
def marker_satisfied?(marker, python_version)
|
|
269
|
+
conditions = marker.split(/\s+(and|or)\s+/)
|
|
270
|
+
|
|
271
|
+
# Explicitly define the type of result as T::Boolean
|
|
272
|
+
result = T.let(evaluate_condition(conditions.shift, python_version), T::Boolean)
|
|
273
|
+
|
|
274
|
+
until conditions.empty?
|
|
275
|
+
operator = conditions.shift
|
|
276
|
+
next_condition = conditions.shift
|
|
277
|
+
next_result = evaluate_condition(next_condition, python_version)
|
|
278
|
+
|
|
279
|
+
result = if operator == "and"
|
|
280
|
+
result && next_result
|
|
281
|
+
else
|
|
282
|
+
result || next_result
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
result
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
sig do
|
|
290
|
+
params(condition: T.untyped,
|
|
291
|
+
python_version: T.any(String, Integer, Gem::Version)).returns(T::Boolean)
|
|
292
|
+
end
|
|
293
|
+
def evaluate_condition(condition, python_version)
|
|
294
|
+
operator, version = condition.match(/([<>=!]=?)\s*"?([\d.]+)"?/)&.captures
|
|
295
|
+
|
|
296
|
+
case operator
|
|
297
|
+
when "<"
|
|
298
|
+
Dependabot::Uv::Version.new(python_version) < Dependabot::Uv::Version.new(version)
|
|
299
|
+
when "<="
|
|
300
|
+
Dependabot::Uv::Version.new(python_version) <= Dependabot::Uv::Version.new(version)
|
|
301
|
+
when ">"
|
|
302
|
+
Dependabot::Uv::Version.new(python_version) > Dependabot::Uv::Version.new(version)
|
|
303
|
+
when ">="
|
|
304
|
+
Dependabot::Uv::Version.new(python_version) >= Dependabot::Uv::Version.new(version)
|
|
305
|
+
when "=="
|
|
306
|
+
Dependabot::Uv::Version.new(python_version) == Dependabot::Uv::Version.new(version)
|
|
307
|
+
else
|
|
308
|
+
false
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
sig { returns(DependencySet) }
|
|
313
|
+
def setup_file_dependencies
|
|
314
|
+
@setup_file_dependencies ||= T.let(SetupFileParser.new(dependency_files: dependency_files)
|
|
315
|
+
.dependency_set, T.nilable(DependencySet))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
sig { returns(T.untyped) }
|
|
319
|
+
def parsed_requirement_files
|
|
320
|
+
SharedHelpers.in_a_temporary_directory do
|
|
321
|
+
write_temporary_dependency_files
|
|
322
|
+
|
|
323
|
+
requirements = SharedHelpers.run_helper_subprocess(
|
|
324
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
|
325
|
+
function: "parse_requirements",
|
|
326
|
+
args: [Dir.pwd]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
check_requirements(requirements)
|
|
330
|
+
requirements
|
|
331
|
+
end
|
|
332
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
333
|
+
evaluation_errors = REQUIREMENT_FILE_EVALUATION_ERRORS
|
|
334
|
+
raise unless e.message.start_with?(*evaluation_errors)
|
|
335
|
+
|
|
336
|
+
raise Dependabot::DependencyFileNotEvaluatable, e.message
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
sig { params(requirements: T.untyped).returns(T.untyped) }
|
|
340
|
+
def check_requirements(requirements)
|
|
341
|
+
requirements.each do |dep|
|
|
342
|
+
next unless dep["requirement"]
|
|
343
|
+
|
|
344
|
+
Uv::Requirement.new(dep["requirement"].split(","))
|
|
345
|
+
rescue Gem::Requirement::BadRequirementError => e
|
|
346
|
+
raise Dependabot::DependencyFileNotEvaluatable, e.message
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
sig { returns(T::Boolean) }
|
|
351
|
+
def pipcompile_in_file
|
|
352
|
+
requirement_files.any? { |f| f.name.end_with?(PackageManager::MANIFEST_FILENAME) }
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
|
356
|
+
def write_temporary_dependency_files
|
|
357
|
+
dependency_files
|
|
358
|
+
.reject { |f| f.name == ".python-version" }
|
|
359
|
+
.each do |file|
|
|
360
|
+
path = file.name
|
|
361
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
|
362
|
+
File.write(path, remove_imports(file))
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
sig { params(file: T.untyped).returns(T.untyped) }
|
|
367
|
+
def remove_imports(file)
|
|
368
|
+
return file.content if file.path.end_with?(".tar.gz", ".whl", ".zip")
|
|
369
|
+
|
|
370
|
+
file.content.lines
|
|
371
|
+
.reject { |l| l.match?(/^['"]?(?<path>\..*?)(?=\[|#|'|"|$)/) }
|
|
372
|
+
.reject { |l| l.match?(/^(?:-e)\s+['"]?(?<path>.*?)(?=\[|#|'|"|$)/) }
|
|
373
|
+
.join
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
sig { params(name: String, extras: T::Array[String]).returns(String) }
|
|
377
|
+
def normalised_name(name, extras = [])
|
|
378
|
+
NameNormaliser.normalise_including_extras(name, extras)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
sig { override.returns(T.untyped) }
|
|
382
|
+
def check_required_files
|
|
383
|
+
filenames = dependency_files.map(&:name)
|
|
384
|
+
return if filenames.any? { |name| name.end_with?(".txt", ".in") }
|
|
385
|
+
return if pipfile
|
|
386
|
+
return if pyproject
|
|
387
|
+
return if setup_file
|
|
388
|
+
return if setup_cfg_file
|
|
389
|
+
|
|
390
|
+
raise "Missing required files!"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
394
|
+
def pipfile
|
|
395
|
+
@pipfile ||= T.let(get_original_file("Pipfile"), T.nilable(Dependabot::DependencyFile))
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
399
|
+
def pipfile_lock
|
|
400
|
+
@pipfile_lock ||= T.let(get_original_file("Pipfile.lock"), T.nilable(Dependabot::DependencyFile))
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
404
|
+
def pyproject
|
|
405
|
+
@pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile))
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
409
|
+
def poetry_lock
|
|
410
|
+
@poetry_lock ||= T.let(get_original_file("poetry.lock"), T.nilable(Dependabot::DependencyFile))
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
414
|
+
def setup_file
|
|
415
|
+
@setup_file ||= T.let(get_original_file("setup.py"), T.nilable(Dependabot::DependencyFile))
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
419
|
+
def setup_cfg_file
|
|
420
|
+
@setup_cfg_file ||= T.let(get_original_file("setup.cfg"), T.nilable(Dependabot::DependencyFile))
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
sig { returns(T::Array[Dependabot::Uv::Requirement]) }
|
|
424
|
+
def pip_compile_files
|
|
425
|
+
@pip_compile_files ||= T.let(dependency_files.select { |f| f.name.end_with?(".in") }, T.untyped)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
sig { returns(Dependabot::Uv::PipCompileFileMatcher) }
|
|
429
|
+
def pip_compile_file_matcher
|
|
430
|
+
@pip_compile_file_matcher ||= T.let(PipCompileFileMatcher.new(pip_compile_files),
|
|
431
|
+
T.nilable(Dependabot::Uv::PipCompileFileMatcher))
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
Dependabot::FileParsers.register("uv", Dependabot::Uv::FileParser)
|