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 +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/file_updater/pipfile_preparer.rb +18 -4
- 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: 88e37f068b87d88c752580a508bbae2850ad20561b9d605806680aefc535976b
|
4
|
+
data.tar.gz: f6e67d072bd9bd1d244566a6788b44f78c204935554bf81c05b47af7fa2e1b32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48b1364b91c163df7bddc902fae51f49984e85eac04e91729025354a51d9472bcc0c04ecf5258c2fa886b2ec157970337727e7a03e5f1a7cfb6358853a5123e7
|
7
|
+
data.tar.gz: 6310f319aff5d5f9d2f3dd0556881623612ed4c6e5321b13acc0b0c988ca7581610abf21c99e5a5bd116d6f4d8fb982405a240d2d40bbde84ee8d5ede9d7a25b
|
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,4 +1,4 @@
|
|
1
|
-
# typed:
|
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:
|
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.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-
|
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.
|
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.
|
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
|
+
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:
|