dependabot-python 0.293.0 → 0.295.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b25ebaf8e9c7ec713cdc0ed55d0447bc0550e6889f1b948626514eb956da21fe
4
- data.tar.gz: 5fa5ef854bd4291ace811e78ce5e48dd33fa34a6dbccc3df13ca993fd67def1c
3
+ metadata.gz: 88e37f068b87d88c752580a508bbae2850ad20561b9d605806680aefc535976b
4
+ data.tar.gz: f6e67d072bd9bd1d244566a6788b44f78c204935554bf81c05b47af7fa2e1b32
5
5
  SHA512:
6
- metadata.gz: a06decf5a991d84e80b454307fdb269927619e24f295a5c759b5403e7cc036d6f143a87d6b0718fb3bcfb9460d625fc3a13acf8aa8bf2c7054205afb25ca1436
7
- data.tar.gz: cd14d822f9f00215a89f0baa2cbcff208e88d04c5c72d6dc18759e6caf974b0f1112625bc37305ebc029c188bc26616d836ad5dc1ca739f8dc7dee4e640679ff
6
+ metadata.gz: 48b1364b91c163df7bddc902fae51f49984e85eac04e91729025354a51d9472bcc0c04ecf5258c2fa886b2ec157970337727e7a03e5f1a7cfb6358853a5123e7
7
+ data.tar.gz: 6310f319aff5d5f9d2f3dd0556881623612ed4c6e5321b13acc0b0c988ca7581610abf21c99e5a5bd116d6f4d8fb982405a240d2d40bbde84ee8d5ede9d7a25b
@@ -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,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
@@ -12,10 +12,14 @@ module Dependabot
12
12
  module Python
13
13
  class FileUpdater
14
14
  class PipfilePreparer
15
+ extend T::Sig
16
+
17
+ sig { params(pipfile_content: String).void }
15
18
  def initialize(pipfile_content:)
16
19
  @pipfile_content = pipfile_content
17
20
  end
18
21
 
22
+ sig { params(credentials: T::Array[T::Hash[String, T.untyped]]).returns(String) }
19
23
  def replace_sources(credentials)
20
24
  pipfile_object = TomlRB.parse(pipfile_content)
21
25
 
@@ -26,6 +30,7 @@ module Dependabot
26
30
  TomlRB.dump(pipfile_object)
27
31
  end
28
32
 
33
+ sig { params(requirement: String).returns(String) }
29
34
  def update_python_requirement(requirement)
30
35
  pipfile_object = TomlRB.parse(pipfile_content)
31
36
 
@@ -39,6 +44,7 @@ module Dependabot
39
44
  TomlRB.dump(pipfile_object)
40
45
  end
41
46
 
47
+ sig { params(parsed_file: String).returns(String) }
42
48
  def update_ssl_requirement(parsed_file)
43
49
  pipfile_object = TomlRB.parse(pipfile_content)
44
50
  parsed_object = TomlRB.parse(parsed_file)
@@ -56,13 +62,19 @@ module Dependabot
56
62
 
57
63
  private
58
64
 
65
+ sig { returns(String) }
59
66
  attr_reader :pipfile_content
60
- attr_reader :lockfile
61
67
 
68
+ sig { returns(T::Array[T::Hash[String, T.untyped]]) }
62
69
  def pipfile_sources
63
- @pipfile_sources ||= TomlRB.parse(pipfile_content).fetch("source", [])
70
+ @pipfile_sources ||= T.let(TomlRB.parse(pipfile_content).fetch("source", []),
71
+ T.nilable(T::Array[T::Hash[String, T.untyped]]))
64
72
  end
65
73
 
74
+ sig do
75
+ params(source: T::Hash[String, T.untyped],
76
+ credentials: T::Array[T::Hash[String, T.untyped]]).returns(T.nilable(T::Hash[String, T.untyped]))
77
+ end
66
78
  def sub_auth_url(source, credentials)
67
79
  if source["url"].include?("${")
68
80
  base_url = source["url"].sub(/\${.*}@/, "")
@@ -79,8 +91,10 @@ module Dependabot
79
91
  source
80
92
  end
81
93
 
94
+ sig { params(credentials: T::Array[T::Hash[String, T.untyped]]).returns(T::Array[T::Hash[String, T.untyped]]) }
82
95
  def config_variable_sources(credentials)
83
- @config_variable_sources ||=
96
+ @config_variable_sources = T.let([], T.nilable(T::Array[T::Hash[String, T.untyped]]))
97
+ @config_variable_sources =
84
98
  credentials.select { |cred| cred["type"] == "python_index" }.map.with_index do |c, i|
85
99
  {
86
100
  "name" => "dependabot-inserted-index-#{i}",
@@ -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.295.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-30 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.295.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.295.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.295.0
294
294
  post_install_message:
295
295
  rdoc_options: []
296
296
  require_paths: