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,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)