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 +4 -4
- data/helpers/lib/parser.py +5 -4
- data/helpers/requirements.txt +2 -2
- data/lib/dependabot/python/file_parser/pipfile_files_parser.rb +40 -19
- data/lib/dependabot/python/file_parser/pyproject_files_parser.rb +61 -25
- data/lib/dependabot/python/file_parser/setup_file_parser.rb +18 -3
- data/lib/dependabot/python/file_parser.rb +71 -43
- data/lib/dependabot/python/language_version_manager.rb +16 -4
- data/lib/dependabot/python/pipenv_runner.rb +25 -3
- data/lib/dependabot/python/update_checker/pip_compile_version_resolver.rb +21 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00afa0b8f378f4c7afd4ef6c9d8e9445829b0cdace6b9ccd4a86673b60a65ba7
|
4
|
+
data.tar.gz: 326e757f7c41bf6d6078423efcab1e0fe078654c5cfe5ffd1baa105eabe0d862
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 012e54234fb1fae65c85fa67b440c408d063489b0b1fa0a91aae33a2618f8c8f201ae822fc85d03fa9079bd0141cb2b5a797c14ffa9b81d248607b803232059b
|
7
|
+
data.tar.gz: 69b326583030cf0e5749425d25c98051a9c9b18470d4f45dec5cd269ba70545d647b9c5c40872b5b772757fcbec53ad56c18ed0a705024f21fdad13cb98816e0
|
data/helpers/lib/parser.py
CHANGED
@@ -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 `
|
18
|
-
# support for Python 3.10.
|
19
|
-
import
|
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
|
-
|
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 = []
|
data/helpers/requirements.txt
CHANGED
@@ -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 `
|
11
|
-
|
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:
|
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
|
-
|
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
|
-
|
145
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
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:
|
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
|
-
|
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
|
-
|
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 {
|
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).
|
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
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
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
|
-
|
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"]
|
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
|
-
|
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:
|
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:
|
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.
|
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-
|
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.
|
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.
|
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
|
+
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:
|