dependabot-python 0.293.0 → 0.294.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b25ebaf8e9c7ec713cdc0ed55d0447bc0550e6889f1b948626514eb956da21fe
4
- data.tar.gz: 5fa5ef854bd4291ace811e78ce5e48dd33fa34a6dbccc3df13ca993fd67def1c
3
+ metadata.gz: 00afa0b8f378f4c7afd4ef6c9d8e9445829b0cdace6b9ccd4a86673b60a65ba7
4
+ data.tar.gz: 326e757f7c41bf6d6078423efcab1e0fe078654c5cfe5ffd1baa105eabe0d862
5
5
  SHA512:
6
- metadata.gz: a06decf5a991d84e80b454307fdb269927619e24f295a5c759b5403e7cc036d6f143a87d6b0718fb3bcfb9460d625fc3a13acf8aa8bf2c7054205afb25ca1436
7
- data.tar.gz: cd14d822f9f00215a89f0baa2cbcff208e88d04c5c72d6dc18759e6caf974b0f1112625bc37305ebc029c188bc26616d836ad5dc1ca739f8dc7dee4e640679ff
6
+ metadata.gz: 012e54234fb1fae65c85fa67b440c408d063489b0b1fa0a91aae33a2618f8c8f201ae822fc85d03fa9079bd0141cb2b5a797c14ffa9b81d248607b803232059b
7
+ data.tar.gz: 69b326583030cf0e5749425d25c98051a9c9b18470d4f45dec5cd269ba70545d647b9c5c40872b5b772757fcbec53ad56c18ed0a705024f21fdad13cb98816e0
@@ -14,9 +14,9 @@ from pip._internal.req.constructors import (
14
14
  )
15
15
 
16
16
  from packaging.requirements import InvalidRequirement, Requirement
17
- # TODO: Replace 3p package `toml` with 3.11's new stdlib `tomllib` once we drop
18
- # support for Python 3.10.
19
- import toml
17
+ # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we
18
+ # drop support for Python 3.10.
19
+ import tomli
20
20
 
21
21
  # Inspired by pips internal check:
22
22
  # https://github.com/pypa/pip/blob/0bb3ac87f5bb149bd75cceac000844128b574385/src/pip/_internal/req/req_file.py#L35
@@ -24,7 +24,8 @@ COMMENT_RE = re.compile(r'(^|\s+)#.*$')
24
24
 
25
25
 
26
26
  def parse_pep621_dependencies(pyproject_path):
27
- project_toml = toml.load(pyproject_path)
27
+ with open(pyproject_path, "rb") as file:
28
+ project_toml = tomli.load(file)
28
29
 
29
30
  def parse_toml_section_pep621_dependencies(pyproject_path, dependencies):
30
31
  requirement_packages = []
@@ -7,8 +7,8 @@ hashin==1.0.3; python_version >= '3.9'
7
7
  pipenv==2024.0.2
8
8
  plette==2.1.0
9
9
  poetry==1.8.5
10
- # TODO: Replace 3p package `toml` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
11
- toml==0.10.2
10
+ # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
11
+ tomli==2.0.1
12
12
 
13
13
  # Some dependencies will only install if Cython is present
14
14
  Cython==3.0.10
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -13,7 +13,8 @@ module Dependabot
13
13
  module Python
14
14
  class FileParser
15
15
  class PipfileFilesParser
16
- DEPENDENCY_GROUP_KEYS = [
16
+ extend T::Sig
17
+ DEPENDENCY_GROUP_KEYS = T.let([
17
18
  {
18
19
  pipfile: "packages",
19
20
  lockfile: "default"
@@ -22,12 +23,14 @@ module Dependabot
22
23
  pipfile: "dev-packages",
23
24
  lockfile: "develop"
24
25
  }
25
- ].freeze
26
+ ].freeze, T::Array[T::Hash[Symbol, String]])
26
27
 
28
+ sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
27
29
  def initialize(dependency_files:)
28
30
  @dependency_files = dependency_files
29
31
  end
30
32
 
33
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
31
34
  def dependency_set
32
35
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
33
36
 
@@ -39,19 +42,21 @@ module Dependabot
39
42
 
40
43
  private
41
44
 
45
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
42
46
  attr_reader :dependency_files
43
47
 
48
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
44
49
  def pipfile_dependencies
45
50
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
46
51
 
47
52
  DEPENDENCY_GROUP_KEYS.each do |keys|
48
- next unless parsed_pipfile[keys[:pipfile]]
53
+ next unless parsed_pipfile[T.must(keys[:pipfile])]
49
54
 
50
- parsed_pipfile[keys[:pipfile]].map do |dep_name, req|
55
+ parsed_pipfile[T.must(keys[:pipfile])].map do |dep_name, req|
51
56
  group = keys[:lockfile]
52
57
  next unless specifies_version?(req)
53
58
  next if git_or_path_requirement?(req)
54
- next if pipfile_lock && !dependency_version(dep_name, req, group)
59
+ next if pipfile_lock && !dependency_version(dep_name, req, T.must(group))
55
60
 
56
61
  # Empty requirements are not allowed in Dependabot::Dependency and
57
62
  # equivalent to "*" (latest available version)
@@ -60,10 +65,10 @@ module Dependabot
60
65
  dependencies <<
61
66
  Dependency.new(
62
67
  name: normalised_name(dep_name),
63
- version: dependency_version(dep_name, req, group),
68
+ version: dependency_version(dep_name, req, T.must(group)),
64
69
  requirements: [{
65
70
  requirement: req.is_a?(String) ? req : req["version"],
66
- file: pipfile.name,
71
+ file: T.must(pipfile).name,
67
72
  source: nil,
68
73
  groups: [group]
69
74
  }],
@@ -79,6 +84,7 @@ module Dependabot
79
84
  # Create a DependencySet where each element has no requirement. Any
80
85
  # requirements will be added when combining the DependencySet with
81
86
  # other DependencySets.
87
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
82
88
  def pipfile_lock_dependencies
83
89
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
84
90
  return dependencies unless pipfile_lock
@@ -108,6 +114,10 @@ module Dependabot
108
114
  dependencies
109
115
  end
110
116
 
117
+ sig do
118
+ params(dep_name: String, requirement: T.any(String, T::Hash[String, T.untyped]),
119
+ group: String).returns(T.nilable(String))
120
+ end
111
121
  def dependency_version(dep_name, requirement, group)
112
122
  req = version_from_hash_or_string(requirement)
113
123
 
@@ -117,11 +127,14 @@ module Dependabot
117
127
 
118
128
  version = version_from_hash_or_string(details)
119
129
  version&.gsub(/^===?/, "")
120
- elsif req.start_with?("==") && !req.include?("*")
121
- req.strip.gsub(/^===?/, "")
130
+ elsif T.must(req).start_with?("==") && !T.must(req).include?("*")
131
+ T.must(req).strip.gsub(/^===?/, "")
122
132
  end
123
133
  end
124
134
 
135
+ sig do
136
+ params(obj: T.any(String, NilClass, T::Array[String], T::Hash[String, T.untyped])).returns(T.nilable(String))
137
+ end
125
138
  def version_from_hash_or_string(obj)
126
139
  case obj
127
140
  when String then obj.strip
@@ -129,41 +142,49 @@ module Dependabot
129
142
  end
130
143
  end
131
144
 
145
+ sig { params(req: T.any(String, T::Hash[String, T.untyped])).returns(T.any(T::Boolean, NilClass, String)) }
132
146
  def specifies_version?(req)
133
147
  return true if req.is_a?(String)
134
148
 
135
149
  req["version"]
136
150
  end
137
151
 
152
+ sig { params(req: T.any(String, T::Hash[String, T.untyped])).returns(T::Boolean) }
138
153
  def git_or_path_requirement?(req)
139
154
  return false unless req.is_a?(Hash)
140
155
 
141
156
  %w(git path).any? { |k| req.key?(k) }
142
157
  end
143
158
 
144
- def normalised_name(name)
145
- NameNormaliser.normalise(name)
159
+ sig { params(name: String, extras: T::Array[String]).returns(String) }
160
+ def normalised_name(name, extras = [])
161
+ NameNormaliser.normalise_including_extras(name, extras)
146
162
  end
147
163
 
164
+ sig { returns(T::Hash[String, T.untyped]) }
148
165
  def parsed_pipfile
149
- @parsed_pipfile ||= TomlRB.parse(pipfile.content)
166
+ @parsed_pipfile ||= T.let(TomlRB.parse(T.must(pipfile).content), T.nilable(T::Hash[String, T.untyped]))
150
167
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
151
- raise Dependabot::DependencyFileNotParseable, pipfile.path
168
+ raise Dependabot::DependencyFileNotParseable, T.must(pipfile).path
152
169
  end
153
170
 
171
+ sig { returns(T::Hash[String, T.untyped]) }
154
172
  def parsed_pipfile_lock
155
- @parsed_pipfile_lock ||= JSON.parse(pipfile_lock.content)
173
+ @parsed_pipfile_lock ||= T.let(JSON.parse(T.must(T.must(pipfile_lock).content)),
174
+ T.nilable(T::Hash[String, T.untyped]))
156
175
  rescue JSON::ParserError
157
- raise Dependabot::DependencyFileNotParseable, pipfile_lock.path
176
+ raise Dependabot::DependencyFileNotParseable, T.must(pipfile_lock).path
158
177
  end
159
178
 
179
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
160
180
  def pipfile
161
- @pipfile ||= dependency_files.find { |f| f.name == "Pipfile" }
181
+ @pipfile ||= T.let(dependency_files.find { |f| f.name == "Pipfile" }, T.nilable(Dependabot::DependencyFile))
162
182
  end
163
183
 
184
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
164
185
  def pipfile_lock
165
- @pipfile_lock ||=
166
- dependency_files.find { |f| f.name == "Pipfile.lock" }
186
+ @pipfile_lock ||= T.let(dependency_files.find { |f| f.name == "Pipfile.lock" },
187
+ T.nilable(Dependabot::DependencyFile))
167
188
  end
168
189
  end
169
190
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -14,15 +14,18 @@ module Dependabot
14
14
  module Python
15
15
  class FileParser
16
16
  class PyprojectFilesParser
17
+ extend T::Sig
17
18
  POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze
18
19
 
19
20
  # https://python-poetry.org/docs/dependency-specification/
20
21
  UNSUPPORTED_DEPENDENCY_TYPES = %w(git path url).freeze
21
22
 
23
+ sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
22
24
  def initialize(dependency_files:)
23
25
  @dependency_files = dependency_files
24
26
  end
25
27
 
28
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
26
29
  def dependency_set
27
30
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
28
31
 
@@ -34,16 +37,18 @@ module Dependabot
34
37
 
35
38
  private
36
39
 
40
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
37
41
  attr_reader :dependency_files
38
42
 
43
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
39
44
  def pyproject_dependencies
40
45
  if using_poetry?
41
46
  missing_keys = missing_poetry_keys
42
47
 
43
48
  if missing_keys.any?
44
49
  raise DependencyFileNotParseable.new(
45
- pyproject.path,
46
- "#{pyproject.path} is missing the following sections:\n" \
50
+ T.must(pyproject).path,
51
+ "#{T.must(pyproject).path} is missing the following sections:\n" \
47
52
  " * #{missing_keys.map { |key| "tool.poetry.#{key}" }.join("\n * ")}\n"
48
53
  )
49
54
  end
@@ -54,25 +59,28 @@ module Dependabot
54
59
  end
55
60
  end
56
61
 
62
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
57
63
  def poetry_dependencies
58
- @poetry_dependencies ||= parse_poetry_dependencies
64
+ @poetry_dependencies ||= T.let(parse_poetry_dependencies, T.untyped)
59
65
  end
60
66
 
67
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
61
68
  def parse_poetry_dependencies
62
69
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
63
70
 
64
71
  POETRY_DEPENDENCY_TYPES.each do |type|
65
- deps_hash = poetry_root[type] || {}
72
+ deps_hash = T.must(poetry_root)[type] || {}
66
73
  dependencies += parse_poetry_dependency_group(type, deps_hash)
67
74
  end
68
75
 
69
- groups = poetry_root["group"] || {}
76
+ groups = T.must(poetry_root)["group"] || {}
70
77
  groups.each do |group, group_spec|
71
78
  dependencies += parse_poetry_dependency_group(group, group_spec["dependencies"])
72
79
  end
73
80
  dependencies
74
81
  end
75
82
 
83
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
76
84
  def pep621_dependencies
77
85
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
78
86
 
@@ -107,6 +115,11 @@ module Dependabot
107
115
  dependencies
108
116
  end
109
117
 
118
+ sig do
119
+ params(type: String,
120
+ deps_hash: T::Hash[String,
121
+ T.untyped]).returns(Dependabot::FileParsers::Base::DependencySet)
122
+ end
110
123
  def parse_poetry_dependency_group(type, deps_hash)
111
124
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
112
125
 
@@ -126,11 +139,13 @@ module Dependabot
126
139
  dependencies
127
140
  end
128
141
 
142
+ sig { params(name: String, extras: T::Array[String]).returns(String) }
129
143
  def normalised_name(name, extras)
130
144
  NameNormaliser.normalise_including_extras(name, extras)
131
145
  end
132
146
 
133
147
  # @param req can be an Array, Hash or String that represents the constraints for a dependency
148
+ sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
134
149
  def parse_requirements_from(req, type)
135
150
  [req].flatten.compact.filter_map do |requirement|
136
151
  next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
@@ -140,14 +155,14 @@ module Dependabot
140
155
  if requirement.is_a?(String)
141
156
  {
142
157
  requirement: requirement,
143
- file: pyproject.name,
158
+ file: T.must(pyproject).name,
144
159
  source: nil,
145
160
  groups: [type]
146
161
  }
147
162
  else
148
163
  {
149
164
  requirement: requirement["version"],
150
- file: pyproject.name,
165
+ file: T.must(pyproject).name,
151
166
  source: requirement.fetch("source", nil),
152
167
  groups: [type]
153
168
  }
@@ -155,26 +170,31 @@ module Dependabot
155
170
  end
156
171
  end
157
172
 
173
+ sig { returns(T.nilable(T::Boolean)) }
158
174
  def using_poetry?
159
175
  !poetry_root.nil?
160
176
  end
161
177
 
178
+ sig { returns(T::Array[String]) }
162
179
  def missing_poetry_keys
163
- package_mode = poetry_root.fetch("package-mode", true)
180
+ package_mode = T.must(poetry_root).fetch("package-mode", true)
164
181
  required_keys = package_mode ? %w(name version description authors) : []
165
- required_keys.reject { |key| poetry_root.key?(key) }
182
+ required_keys.reject { |key| T.must(poetry_root).key?(key) }
166
183
  end
167
184
 
185
+ sig { returns(T::Boolean) }
168
186
  def using_pep621?
169
187
  !parsed_pyproject.dig("project", "dependencies").nil? ||
170
188
  !parsed_pyproject.dig("project", "optional-dependencies").nil? ||
171
189
  !parsed_pyproject.dig("build-system", "requires").nil?
172
190
  end
173
191
 
192
+ sig { returns(T.nilable(T::Hash[String, T.untyped])) }
174
193
  def poetry_root
175
194
  parsed_pyproject.dig("tool", "poetry")
176
195
  end
177
196
 
197
+ sig { returns(T.untyped) }
178
198
  def using_pdm?
179
199
  using_pep621? && pdm_lock
180
200
  end
@@ -182,6 +202,7 @@ module Dependabot
182
202
  # Create a DependencySet where each element has no requirement. Any
183
203
  # requirements will be added when combining the DependencySet with
184
204
  # other DependencySets.
205
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
185
206
  def lockfile_dependencies
186
207
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
187
208
 
@@ -206,13 +227,16 @@ module Dependabot
206
227
  dependencies
207
228
  end
208
229
 
230
+ sig { returns(T::Array[T.nilable(String)]) }
209
231
  def production_dependency_names
210
- @production_dependency_names ||= parse_production_dependency_names
232
+ @production_dependency_names ||= T.let(parse_production_dependency_names,
233
+ T.nilable(T::Array[T.nilable(String)]))
211
234
  end
212
235
 
236
+ sig { returns(T::Array[T.nilable(String)]) }
213
237
  def parse_production_dependency_names
214
238
  SharedHelpers.in_a_temporary_directory do
215
- File.write(pyproject.name, pyproject.content)
239
+ File.write(T.must(pyproject).name, T.must(pyproject).content)
216
240
  File.write(lockfile.name, lockfile.content)
217
241
 
218
242
  begin
@@ -232,6 +256,7 @@ module Dependabot
232
256
  end
233
257
  end
234
258
 
259
+ sig { params(dep_name: String).returns(T.untyped) }
235
260
  def version_from_lockfile(dep_name)
236
261
  return unless parsed_lockfile
237
262
 
@@ -240,6 +265,7 @@ module Dependabot
240
265
  &.fetch("version", nil)
241
266
  end
242
267
 
268
+ sig { params(req: T.untyped).returns(T::Array[Dependabot::Python::Requirement]) }
243
269
  def check_requirements(req)
244
270
  requirement = req.is_a?(String) ? req : req["version"]
245
271
  Python::Requirement.requirements_array(requirement)
@@ -247,31 +273,37 @@ module Dependabot
247
273
  raise Dependabot::DependencyFileNotEvaluatable, e.message
248
274
  end
249
275
 
276
+ sig { params(name: String).returns(String) }
250
277
  def normalise(name)
251
278
  NameNormaliser.normalise(name)
252
279
  end
253
280
 
281
+ sig { returns(T.untyped) }
254
282
  def parsed_pyproject
255
- @parsed_pyproject ||= TomlRB.parse(pyproject.content)
283
+ @parsed_pyproject ||= T.let(TomlRB.parse(T.must(pyproject).content), T.untyped)
256
284
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
257
- raise Dependabot::DependencyFileNotParseable, pyproject.path
285
+ raise Dependabot::DependencyFileNotParseable, T.must(pyproject).path
258
286
  end
259
287
 
288
+ sig { returns(T.untyped) }
260
289
  def parsed_poetry_lock
261
- @parsed_poetry_lock ||= TomlRB.parse(poetry_lock.content)
290
+ @parsed_poetry_lock ||= T.let(TomlRB.parse(T.must(poetry_lock).content), T.untyped)
262
291
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
263
- raise Dependabot::DependencyFileNotParseable, poetry_lock.path
292
+ raise Dependabot::DependencyFileNotParseable, T.must(poetry_lock).path
264
293
  end
265
294
 
295
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
266
296
  def pyproject
267
- @pyproject ||=
268
- dependency_files.find { |f| f.name == "pyproject.toml" }
297
+ @pyproject ||= T.let(dependency_files.find { |f| f.name == "pyproject.toml" },
298
+ T.nilable(Dependabot::DependencyFile))
269
299
  end
270
300
 
301
+ sig { returns(T.untyped) }
271
302
  def lockfile
272
303
  poetry_lock
273
304
  end
274
305
 
306
+ sig { returns(T.untyped) }
275
307
  def parsed_pep621_dependencies
276
308
  SharedHelpers.in_a_temporary_directory do
277
309
  write_temporary_pyproject
@@ -279,29 +311,33 @@ module Dependabot
279
311
  SharedHelpers.run_helper_subprocess(
280
312
  command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
281
313
  function: "parse_pep621_dependencies",
282
- args: [pyproject.name]
314
+ args: [T.must(pyproject).name]
283
315
  )
284
316
  end
285
317
  end
286
318
 
319
+ sig { returns(Integer) }
287
320
  def write_temporary_pyproject
288
- path = pyproject.name
321
+ path = T.must(pyproject).name
289
322
  FileUtils.mkdir_p(Pathname.new(path).dirname)
290
- File.write(path, pyproject.content)
323
+ File.write(path, T.must(pyproject).content)
291
324
  end
292
325
 
326
+ sig { returns(T.untyped) }
293
327
  def parsed_lockfile
294
328
  parsed_poetry_lock if poetry_lock
295
329
  end
296
330
 
331
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
297
332
  def poetry_lock
298
- @poetry_lock ||=
299
- dependency_files.find { |f| f.name == "poetry.lock" }
333
+ @poetry_lock ||= T.let(dependency_files.find { |f| f.name == "poetry.lock" },
334
+ T.nilable(Dependabot::DependencyFile))
300
335
  end
301
336
 
337
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
302
338
  def pdm_lock
303
- @pdm_lock ||=
304
- dependency_files.find { |f| f.name == "pdm.lock" }
339
+ @pdm_lock ||= T.let(dependency_files.find { |f| f.name == "pdm.lock" },
340
+ T.nilable(Dependabot::DependencyFile))
305
341
  end
306
342
  end
307
343
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
@@ -8,22 +8,26 @@ require "dependabot/shared_helpers"
8
8
  require "dependabot/python/file_parser"
9
9
  require "dependabot/python/native_helpers"
10
10
  require "dependabot/python/name_normaliser"
11
+ require "sorbet-runtime"
11
12
 
12
13
  module Dependabot
13
14
  module Python
14
15
  class FileParser
15
16
  class SetupFileParser
17
+ extend T::Sig
16
18
  INSTALL_REQUIRES_REGEX = /install_requires\s*=\s*\[/m
17
19
  SETUP_REQUIRES_REGEX = /setup_requires\s*=\s*\[/m
18
20
  TESTS_REQUIRE_REGEX = /tests_require\s*=\s*\[/m
19
21
  EXTRAS_REQUIRE_REGEX = /extras_require\s*=\s*\{/m
20
22
 
21
- CLOSING_BRACKET = { "[" => "]", "{" => "}" }.freeze
23
+ CLOSING_BRACKET = T.let({ "[" => "]", "{" => "}" }.freeze, T.any(T.untyped, T.untyped))
22
24
 
25
+ sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
23
26
  def initialize(dependency_files:)
24
27
  @dependency_files = dependency_files
25
28
  end
26
29
 
30
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
27
31
  def dependency_set
28
32
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
29
33
 
@@ -54,8 +58,10 @@ module Dependabot
54
58
 
55
59
  private
56
60
 
61
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
57
62
  attr_reader :dependency_files
58
63
 
64
+ sig { returns(T.untyped) }
59
65
  def parsed_setup_file
60
66
  SharedHelpers.in_a_temporary_directory do
61
67
  write_temporary_dependency_files
@@ -77,6 +83,7 @@ module Dependabot
77
83
  parsed_sanitized_setup_file
78
84
  end
79
85
 
86
+ sig { returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
80
87
  def parsed_sanitized_setup_file
81
88
  SharedHelpers.in_a_temporary_directory do
82
89
  write_sanitized_setup_file
@@ -98,8 +105,9 @@ module Dependabot
98
105
  []
99
106
  end
100
107
 
108
+ sig { params(requirements: T.untyped).returns(T.untyped) }
101
109
  def check_requirements(requirements)
102
- requirements.each do |dep|
110
+ requirements&.each do |dep|
103
111
  next unless dep["requirement"]
104
112
 
105
113
  Python::Requirement.new(dep["requirement"].split(","))
@@ -108,6 +116,7 @@ module Dependabot
108
116
  end
109
117
  end
110
118
 
119
+ sig { void }
111
120
  def write_temporary_dependency_files
112
121
  dependency_files
113
122
  .reject { |f| f.name == ".python-version" }
@@ -123,6 +132,7 @@ module Dependabot
123
132
  # This sanitization is far from perfect (it will fail if any of the
124
133
  # entries are dynamic), but it is an alternative approach to the one
125
134
  # used in parser.py which sometimes succeeds when that has failed.
135
+ sig { void }
126
136
  def write_sanitized_setup_file
127
137
  install_requires = get_regexed_req_array(INSTALL_REQUIRES_REGEX)
128
138
  setup_requires = get_regexed_req_array(SETUP_REQUIRES_REGEX)
@@ -141,18 +151,21 @@ module Dependabot
141
151
  File.write("setup.py", tmp)
142
152
  end
143
153
 
154
+ sig { params(regex: Regexp).returns(T.nilable(String)) }
144
155
  def get_regexed_req_array(regex)
145
156
  return unless (mch = setup_file.content.match(regex))
146
157
 
147
158
  "[#{mch.post_match[0..closing_bracket_index(mch.post_match, '[')]}"
148
159
  end
149
160
 
161
+ sig { params(regex: Regexp).returns(T.nilable(String)) }
150
162
  def get_regexed_req_dict(regex)
151
163
  return unless (mch = setup_file.content.match(regex))
152
164
 
153
165
  "{#{mch.post_match[0..closing_bracket_index(mch.post_match, '{')]}"
154
166
  end
155
167
 
168
+ sig { params(string: String, bracket: String).returns(Integer) }
156
169
  def closing_bracket_index(string, bracket)
157
170
  closes_required = 1
158
171
 
@@ -165,10 +178,12 @@ module Dependabot
165
178
  0
166
179
  end
167
180
 
181
+ sig { params(name: String, extras: T::Array[String]).returns(String) }
168
182
  def normalised_name(name, extras)
169
183
  NameNormaliser.normalise_including_extras(name, extras)
170
184
  end
171
185
 
186
+ sig { returns(T.untyped) }
172
187
  def setup_file
173
188
  dependency_files.find { |f| f.name == "setup.py" }
174
189
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
@@ -17,13 +17,14 @@ require "dependabot/python/package_manager"
17
17
 
18
18
  module Dependabot
19
19
  module Python
20
- class FileParser < Dependabot::FileParsers::Base
20
+ class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/ClassLength
21
+ extend T::Sig
21
22
  require_relative "file_parser/pipfile_files_parser"
22
23
  require_relative "file_parser/pyproject_files_parser"
23
24
  require_relative "file_parser/setup_file_parser"
24
25
  require_relative "file_parser/python_requirement_parser"
25
26
 
26
- DEPENDENCY_GROUP_KEYS = [
27
+ DEPENDENCY_GROUP_KEYS = T.let([
27
28
  {
28
29
  pipfile: "packages",
29
30
  lockfile: "default"
@@ -32,7 +33,7 @@ module Dependabot
32
33
  pipfile: "dev-packages",
33
34
  lockfile: "develop"
34
35
  }
35
- ].freeze
36
+ ].freeze, T::Array[T::Hash[Symbol, String]])
36
37
  REQUIREMENT_FILE_EVALUATION_ERRORS = %w(
37
38
  InstallationError RequirementsFileParseError InvalidMarker
38
39
  InvalidRequirement ValueError RecursionError
@@ -43,6 +44,7 @@ module Dependabot
43
44
  # in any way if any metric collection exception start happening
44
45
  UNDETECTED_PACKAGE_MANAGER_VERSION = "0.0"
45
46
 
47
+ sig { override.returns(T::Array[Dependabot::Dependency]) }
46
48
  def parse
47
49
  # TODO: setup.py from external dependencies is evaluated. Provide guards before removing this.
48
50
  raise Dependabot::UnexpectedExternalCode if @reject_external_code
@@ -57,7 +59,7 @@ module Dependabot
57
59
  dependency_set.dependencies
58
60
  end
59
61
 
60
- sig { returns(Ecosystem) }
62
+ sig { override.returns(Ecosystem) }
61
63
  def ecosystem
62
64
  @ecosystem ||= T.let(
63
65
  Ecosystem.new(
@@ -71,18 +73,16 @@ module Dependabot
71
73
 
72
74
  private
73
75
 
76
+ sig { returns(Dependabot::Python::LanguageVersionManager) }
74
77
  def language_version_manager
75
- @language_version_manager ||=
76
- LanguageVersionManager.new(
77
- python_requirement_parser: python_requirement_parser
78
- )
78
+ @language_version_manager ||= T.let(LanguageVersionManager.new(python_requirement_parser:
79
+ python_requirement_parser), T.nilable(LanguageVersionManager))
79
80
  end
80
81
 
82
+ sig { returns(Dependabot::Python::FileParser::PythonRequirementParser) }
81
83
  def python_requirement_parser
82
- @python_requirement_parser ||=
83
- FileParser::PythonRequirementParser.new(
84
- dependency_files: dependency_files
85
- )
84
+ @python_requirement_parser ||= T.let(FileParser::PythonRequirementParser.new(dependency_files:
85
+ dependency_files), T.nilable(FileParser::PythonRequirementParser))
86
86
  end
87
87
 
88
88
  sig { returns(Ecosystem::VersionManager) }
@@ -91,7 +91,7 @@ module Dependabot
91
91
  Dependabot.logger.info("Detected package manager : #{detected_package_manager.name}")
92
92
  end
93
93
 
94
- @package_manager ||= detected_package_manager
94
+ @package_manager ||= T.let(detected_package_manager, T.nilable(Dependabot::Ecosystem::VersionManager))
95
95
  end
96
96
 
97
97
  sig { returns(Ecosystem::VersionManager) }
@@ -188,7 +188,7 @@ module Dependabot
188
188
  end
189
189
 
190
190
  # setup python local setup on file parser stage
191
- sig { void }
191
+ sig { returns(T.nilable(String)) }
192
192
  def setup_python_environment
193
193
  language_version_manager.install_required_python
194
194
 
@@ -198,14 +198,15 @@ module Dependabot
198
198
  nil
199
199
  end
200
200
 
201
- sig { params(package_manager: String, version: String).void }
201
+ sig { params(package_manager: String, version: String).returns(T::Boolean) }
202
202
  def log_if_version_malformed(package_manager, version)
203
203
  # logs warning if malformed version is found
204
- return true if version.match?(/^\d+(?:\.\d+)*$/)
205
-
206
- Dependabot.logger.warn(
207
- "Detected #{package_manager} with malformed version #{version}"
208
- )
204
+ if version.match?(/^\d+(?:\.\d+)*$/)
205
+ true
206
+ else
207
+ Dependabot.logger.warn("Detected #{package_manager} with malformed version #{version}")
208
+ false
209
+ end
209
210
  end
210
211
 
211
212
  sig { returns(String) }
@@ -231,24 +232,24 @@ module Dependabot
231
232
  )
232
233
  end
233
234
 
235
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
234
236
  def requirement_files
235
237
  dependency_files.select { |f| f.name.end_with?(".txt", ".in") }
236
238
  end
237
239
 
240
+ sig { returns(DependencySet) }
238
241
  def pipenv_dependencies
239
- @pipenv_dependencies ||=
240
- PipfileFilesParser
241
- .new(dependency_files: dependency_files)
242
- .dependency_set
242
+ @pipenv_dependencies ||= T.let(PipfileFilesParser.new(dependency_files:
243
+ dependency_files).dependency_set, T.nilable(DependencySet))
243
244
  end
244
245
 
246
+ sig { returns(DependencySet) }
245
247
  def pyproject_file_dependencies
246
- @pyproject_file_dependencies ||=
247
- PyprojectFilesParser
248
- .new(dependency_files: dependency_files)
249
- .dependency_set
248
+ @pyproject_file_dependencies ||= T.let(PyprojectFilesParser.new(dependency_files:
249
+ dependency_files).dependency_set, T.nilable(DependencySet))
250
250
  end
251
251
 
252
+ sig { returns(DependencySet) }
252
253
  def requirement_dependencies
253
254
  dependencies = DependencySet.new
254
255
  parsed_requirement_files.each do |dep|
@@ -286,6 +287,7 @@ module Dependabot
286
287
  dependencies
287
288
  end
288
289
 
290
+ sig { params(name: T.nilable(String), version: T.nilable(String)).returns(T::Boolean) }
289
291
  def old_pyyaml?(name, version)
290
292
  major_version = version&.split(".")&.first
291
293
  return false unless major_version
@@ -293,6 +295,7 @@ module Dependabot
293
295
  name == "pyyaml" && major_version < "6"
294
296
  end
295
297
 
298
+ sig { params(filename: String).returns(T::Array[String]) }
296
299
  def group_from_filename(filename)
297
300
  if filename.include?("dev") then ["dev-dependencies"]
298
301
  else
@@ -300,6 +303,7 @@ module Dependabot
300
303
  end
301
304
  end
302
305
 
306
+ sig { params(dep: T.untyped).returns(T::Boolean) }
303
307
  def blocking_marker?(dep)
304
308
  return false if dep["markers"] == "None"
305
309
 
@@ -311,11 +315,15 @@ module Dependabot
311
315
  else
312
316
  return true if dep["markers"].include?("<")
313
317
  return false if dep["markers"].include?(">")
318
+ return false if dep["requirement"].nil?
314
319
 
315
- dep["requirement"]&.include?("<")
320
+ dep["requirement"].include?("<")
316
321
  end
317
322
  end
318
323
 
324
+ sig do
325
+ params(marker: T.untyped, python_version: T.any(String, Integer, Gem::Version)).returns(T::Boolean)
326
+ end
319
327
  def marker_satisfied?(marker, python_version)
320
328
  conditions = marker.split(/\s+(and|or)\s+/)
321
329
 
@@ -337,6 +345,10 @@ module Dependabot
337
345
  result
338
346
  end
339
347
 
348
+ sig do
349
+ params(condition: T.untyped,
350
+ python_version: T.any(String, Integer, Gem::Version)).returns(T::Boolean)
351
+ end
340
352
  def evaluate_condition(condition, python_version)
341
353
  operator, version = condition.match(/([<>=!]=?)\s*"?([\d.]+)"?/)&.captures
342
354
 
@@ -356,13 +368,13 @@ module Dependabot
356
368
  end
357
369
  end
358
370
 
371
+ sig { returns(DependencySet) }
359
372
  def setup_file_dependencies
360
- @setup_file_dependencies ||=
361
- SetupFileParser
362
- .new(dependency_files: dependency_files)
363
- .dependency_set
373
+ @setup_file_dependencies ||= T.let(SetupFileParser.new(dependency_files: dependency_files)
374
+ .dependency_set, T.nilable(DependencySet))
364
375
  end
365
376
 
377
+ sig { returns(T.untyped) }
366
378
  def parsed_requirement_files
367
379
  SharedHelpers.in_a_temporary_directory do
368
380
  write_temporary_dependency_files
@@ -383,6 +395,7 @@ module Dependabot
383
395
  raise Dependabot::DependencyFileNotEvaluatable, e.message
384
396
  end
385
397
 
398
+ sig { params(requirements: T.untyped).returns(T.untyped) }
386
399
  def check_requirements(requirements)
387
400
  requirements.each do |dep|
388
401
  next unless dep["requirement"]
@@ -393,18 +406,22 @@ module Dependabot
393
406
  end
394
407
  end
395
408
 
409
+ sig { returns(T::Boolean) }
396
410
  def pipcompile_in_file
397
411
  requirement_files.any? { |f| f.name.end_with?(PipCompilePackageManager::MANIFEST_FILENAME) }
398
412
  end
399
413
 
414
+ sig { returns(T::Boolean) }
400
415
  def pipenv_files
401
416
  dependency_files.any? { |f| f.name == PipenvPackageManager::LOCKFILE_FILENAME }
402
417
  end
403
418
 
419
+ sig { returns(T.nilable(TrueClass)) }
404
420
  def poetry_files
405
421
  true if get_original_file(PoetryPackageManager::LOCKFILE_NAME)
406
422
  end
407
423
 
424
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
408
425
  def write_temporary_dependency_files
409
426
  dependency_files
410
427
  .reject { |f| f.name == ".python-version" }
@@ -415,6 +432,7 @@ module Dependabot
415
432
  end
416
433
  end
417
434
 
435
+ sig { params(file: T.untyped).returns(T.untyped) }
418
436
  def remove_imports(file)
419
437
  return file.content if file.path.end_with?(".tar.gz", ".whl", ".zip")
420
438
 
@@ -424,10 +442,12 @@ module Dependabot
424
442
  .join
425
443
  end
426
444
 
445
+ sig { params(name: String, extras: T::Array[String]).returns(String) }
427
446
  def normalised_name(name, extras = [])
428
447
  NameNormaliser.normalise_including_extras(name, extras)
429
448
  end
430
449
 
450
+ sig { override.returns(T.untyped) }
431
451
  def check_required_files
432
452
  filenames = dependency_files.map(&:name)
433
453
  return if filenames.any? { |name| name.end_with?(".txt", ".in") }
@@ -439,37 +459,45 @@ module Dependabot
439
459
  raise "Missing required files!"
440
460
  end
441
461
 
462
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
442
463
  def pipfile
443
- @pipfile ||= get_original_file("Pipfile")
464
+ @pipfile ||= T.let(get_original_file("Pipfile"), T.nilable(Dependabot::DependencyFile))
444
465
  end
445
466
 
467
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
446
468
  def pipfile_lock
447
- @pipfile_lock ||= get_original_file("Pipfile.lock")
469
+ @pipfile_lock ||= T.let(get_original_file("Pipfile.lock"), T.nilable(Dependabot::DependencyFile))
448
470
  end
449
471
 
472
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
450
473
  def pyproject
451
- @pyproject ||= get_original_file("pyproject.toml")
474
+ @pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile))
452
475
  end
453
476
 
477
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
454
478
  def poetry_lock
455
- @poetry_lock ||= get_original_file("poetry.lock")
479
+ @poetry_lock ||= T.let(get_original_file("poetry.lock"), T.nilable(Dependabot::DependencyFile))
456
480
  end
457
481
 
482
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
458
483
  def setup_file
459
- @setup_file ||= get_original_file("setup.py")
484
+ @setup_file ||= T.let(get_original_file("setup.py"), T.nilable(Dependabot::DependencyFile))
460
485
  end
461
486
 
487
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
462
488
  def setup_cfg_file
463
- @setup_cfg_file ||= get_original_file("setup.cfg")
489
+ @setup_cfg_file ||= T.let(get_original_file("setup.cfg"), T.nilable(Dependabot::DependencyFile))
464
490
  end
465
491
 
492
+ sig { returns(T::Array[Dependabot::Python::Requirement]) }
466
493
  def pip_compile_files
467
- @pip_compile_files ||=
468
- dependency_files.select { |f| f.name.end_with?(".in") }
494
+ @pip_compile_files ||= T.let(dependency_files.select { |f| f.name.end_with?(".in") }, T.untyped)
469
495
  end
470
496
 
497
+ sig { returns(Dependabot::Python::PipCompileFileMatcher) }
471
498
  def pip_compile_file_matcher
472
- @pip_compile_file_matcher ||= PipCompileFileMatcher.new(pip_compile_files)
499
+ @pip_compile_file_matcher ||= T.let(PipCompileFileMatcher.new(pip_compile_files),
500
+ T.nilable(Dependabot::Python::PipCompileFileMatcher))
473
501
  end
474
502
  end
475
503
  end
@@ -1,12 +1,14 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/logger"
5
5
  require "dependabot/python/version"
6
+ require "sorbet-runtime"
6
7
 
7
8
  module Dependabot
8
9
  module Python
9
10
  class LanguageVersionManager
11
+ extend T::Sig
10
12
  # This list must match the versions specified at the top of `python/Dockerfile`
11
13
  PRE_INSTALLED_PYTHON_VERSIONS = %w(
12
14
  3.13.1
@@ -17,10 +19,12 @@ module Dependabot
17
19
  3.8.20
18
20
  ).freeze
19
21
 
22
+ sig { params(python_requirement_parser: T.untyped).void }
20
23
  def initialize(python_requirement_parser:)
21
24
  @python_requirement_parser = python_requirement_parser
22
25
  end
23
26
 
27
+ sig { returns(T.nilable(String)) }
24
28
  def install_required_python
25
29
  # The leading space is important in the version check
26
30
  return if SharedHelpers.run_shell_command("pyenv versions").include?(" #{python_major_minor}.")
@@ -30,22 +34,26 @@ module Dependabot
30
34
  )
31
35
  end
32
36
 
37
+ sig { returns(String) }
33
38
  def installed_version
34
39
  # Use `pyenv exec` to query the active Python version
35
40
  output, _status = SharedHelpers.run_shell_command("pyenv exec python --version")
36
41
  version = output.strip.split.last # Extract the version number (e.g., "3.13.1")
37
42
 
38
- version
43
+ T.must(version)
39
44
  end
40
45
 
46
+ sig { returns(T.untyped) }
41
47
  def python_major_minor
42
- @python_major_minor ||= T.must(Python::Version.new(python_version).segments[0..1]).join(".")
48
+ @python_major_minor ||= T.let(T.must(Python::Version.new(python_version).segments[0..1]).join("."), T.untyped)
43
49
  end
44
50
 
51
+ sig { returns(String) }
45
52
  def python_version
46
- @python_version ||= python_version_from_supported_versions
53
+ @python_version ||= T.let(python_version_from_supported_versions, T.nilable(String))
47
54
  end
48
55
 
56
+ sig { returns(String) }
49
57
  def python_requirement_string
50
58
  if user_specified_python_version
51
59
  if user_specified_python_version.start_with?(/\d/)
@@ -59,6 +67,7 @@ module Dependabot
59
67
  end
60
68
  end
61
69
 
70
+ sig { returns(String) }
62
71
  def python_version_from_supported_versions
63
72
  requirement_string = python_requirement_string
64
73
 
@@ -76,10 +85,12 @@ module Dependabot
76
85
  raise ToolVersionNotSupported.new("Python", python_requirement_string, supported_versions)
77
86
  end
78
87
 
88
+ sig { returns(T.untyped) }
79
89
  def user_specified_python_version
80
90
  @python_requirement_parser.user_specified_requirements.first
81
91
  end
82
92
 
93
+ sig { returns(T.nilable(String)) }
83
94
  def python_version_matching_imputed_requirements
84
95
  compiled_file_python_requirement_markers =
85
96
  @python_requirement_parser.imputed_requirements.map do |r|
@@ -88,6 +99,7 @@ module Dependabot
88
99
  python_version_matching(compiled_file_python_requirement_markers)
89
100
  end
90
101
 
102
+ sig { params(requirements: T.untyped).returns(T.nilable(String)) }
91
103
  def python_version_matching(requirements)
92
104
  PRE_INSTALLED_PYTHON_VERSIONS.find do |version_string|
93
105
  version = Python::Version.new(version_string)
@@ -1,19 +1,31 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/shared_helpers"
5
5
  require "dependabot/python/file_parser"
6
6
  require "json"
7
+ require "sorbet-runtime"
7
8
 
8
9
  module Dependabot
9
10
  module Python
10
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
11
22
  def initialize(dependency:, lockfile:, language_version_manager:)
12
23
  @dependency = dependency
13
24
  @lockfile = lockfile
14
25
  @language_version_manager = language_version_manager
15
26
  end
16
27
 
28
+ sig { params(constraint: String).returns(String) }
17
29
  def run_upgrade(constraint)
18
30
  constraint = "" if constraint == "*"
19
31
  command = "pyenv exec pipenv upgrade --verbose #{dependency_name}#{constraint}"
@@ -22,6 +34,7 @@ module Dependabot
22
34
  run(command, fingerprint: "pyenv exec pipenv upgrade --verbose <dependency_name><constraint>")
23
35
  end
24
36
 
37
+ sig { params(constraint: String).returns(T.nilable(String)) }
25
38
  def run_upgrade_and_fetch_version(constraint)
26
39
  run_upgrade(constraint)
27
40
 
@@ -30,6 +43,7 @@ module Dependabot
30
43
  fetch_version_from_parsed_lockfile(updated_lockfile)
31
44
  end
32
45
 
46
+ sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
33
47
  def run(command, fingerprint: nil)
34
48
  run_command(
35
49
  "pyenv local #{language_version_manager.python_major_minor}",
@@ -41,10 +55,14 @@ module Dependabot
41
55
 
42
56
  private
43
57
 
58
+ sig { returns(Dependabot::Dependency) }
44
59
  attr_reader :dependency
60
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
45
61
  attr_reader :lockfile
62
+ sig { returns(LanguageVersionManager) }
46
63
  attr_reader :language_version_manager
47
64
 
65
+ sig { params(updated_lockfile: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
48
66
  def fetch_version_from_parsed_lockfile(updated_lockfile)
49
67
  deps = updated_lockfile[lockfile_section] || {}
50
68
 
@@ -52,25 +70,29 @@ module Dependabot
52
70
  &.gsub(/^==/, "")
53
71
  end
54
72
 
73
+ sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
55
74
  def run_command(command, fingerprint: nil)
56
75
  SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
57
76
  end
58
77
 
78
+ sig { returns(String) }
59
79
  def lockfile_section
60
80
  if dependency.requirements.any?
61
- dependency.requirements.first[:groups].first
81
+ T.must(dependency.requirements.first)[:groups].first
62
82
  else
63
83
  Python::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
64
84
  section = keys.fetch(:lockfile)
65
- return section if JSON.parse(lockfile.content)[section].keys.any?(dependency_name)
85
+ return section if JSON.parse(T.must(T.must(lockfile).content))[section].keys.any?(dependency_name)
66
86
  end
67
87
  end
68
88
  end
69
89
 
90
+ sig { returns(String) }
70
91
  def dependency_name
71
92
  dependency.metadata[:original_name] || dependency.name
72
93
  end
73
94
 
95
+ sig { returns(T::Hash[String, String]) }
74
96
  def pipenv_env_variables
75
97
  {
76
98
  "PIPENV_YES" => "true", # Install new Python ver if needed
@@ -37,6 +37,7 @@ module Dependabot
37
37
  attr_reader :dependency_files
38
38
  attr_reader :credentials
39
39
  attr_reader :repo_contents_path
40
+ attr_reader :error_handler
40
41
 
41
42
  def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:)
42
43
  @dependency = dependency
@@ -44,6 +45,7 @@ module Dependabot
44
45
  @credentials = credentials
45
46
  @repo_contents_path = repo_contents_path
46
47
  @build_isolation = true
48
+ @error_handler = PipCompileErrorHandler.new
47
49
  end
48
50
 
49
51
  def latest_resolvable_version(requirement: nil)
@@ -186,6 +188,8 @@ module Dependabot
186
188
 
187
189
  raise Dependabot::OutOfMemory if message.end_with?("MemoryError")
188
190
 
191
+ error_handler.handle_pipcompile_error(message)
192
+
189
193
  raise
190
194
  end
191
195
  # rubocop:enable Metrics/AbcSize
@@ -494,5 +498,22 @@ module Dependabot
494
498
  end
495
499
  end
496
500
  end
501
+
502
+ class PipCompileErrorHandler
503
+ SUBPROCESS_ERROR = /subprocess-exited-with-error/
504
+
505
+ INSTALLATION_ERROR = /InstallationError/
506
+
507
+ INSTALLATION_SUBPROCESS_ERROR = /InstallationSubprocessError/
508
+
509
+ HASH_MISMATCH = /HashMismatch/
510
+
511
+ def handle_pipcompile_error(error)
512
+ return unless error.match?(SUBPROCESS_ERROR) || error.match?(INSTALLATION_ERROR) ||
513
+ error.match?(INSTALLATION_SUBPROCESS_ERROR) || error.match?(HASH_MISMATCH)
514
+
515
+ raise DependencyFileNotResolvable, "Error resolving dependency"
516
+ end
517
+ end
497
518
  end
498
519
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-python
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.293.0
4
+ version: 0.294.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-16 00:00:00.000000000 Z
11
+ date: 2025-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.293.0
19
+ version: 0.294.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.293.0
26
+ version: 0.294.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -290,7 +290,7 @@ licenses:
290
290
  - MIT
291
291
  metadata:
292
292
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
293
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.293.0
293
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.294.0
294
294
  post_install_message:
295
295
  rdoc_options: []
296
296
  require_paths: