dependabot-python 0.393.0 → 0.394.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/requirements.txt +2 -2
- data/lib/dependabot/python/file_parser/pep_dependency.rb +53 -0
- data/lib/dependabot/python/file_parser/poetry_lock.rb +95 -0
- data/lib/dependabot/python/file_parser/pyproject_document.rb +242 -0
- data/lib/dependabot/python/file_parser/pyproject_files_parser.rb +134 -128
- data/lib/dependabot/python/file_parser.rb +3 -0
- data/lib/dependabot/python/package/package_details_fetcher.rb +81 -35
- data/lib/dependabot/python/package/package_registry_finder.rb +11 -4
- data/lib/dependabot/python/package/simple_api_parser.rb +91 -0
- data/lib/dependabot/python/shared_file_fetcher.rb +1 -2
- data/lib/dependabot/python/update_checker/latest_version_finder.rb +16 -0
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d1b1deb24d07d3b15f0c1cb2a434bc4014459ff9e52173a6b1aa1608bed9bef
|
|
4
|
+
data.tar.gz: c2a82f5e34e3f06ecfd16ad0516aa925d7cea4d564b9436befaf50ea5eaeb05f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6f557bb606f5cdd0105035e2569dfb80410716ae0b6eb514c46d92837f4677107e2f22c309dd90fcd983c0b4a0d23a8ef596036f242560670de569b90a01b43
|
|
7
|
+
data.tar.gz: 7426a6a6748ef14ed637e82fa5757e5a11fc2c56fadb9e549b559a35e0bd831985978be092f6908068ae5cdd787d330ff95bc7e9b54dc0add588523a4a194d6d
|
data/helpers/requirements.txt
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
require "dependabot/python/file_parser"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Python
|
|
10
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
11
|
+
class PepDependency < T::ImmutableStruct
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
const :name, String
|
|
15
|
+
const :version, T.nilable(String), default: nil
|
|
16
|
+
const :markers, T.nilable(String), default: nil
|
|
17
|
+
const :file, String
|
|
18
|
+
const :requirement, T.nilable(String), default: nil
|
|
19
|
+
const :source_requirement, T.nilable(String), default: nil
|
|
20
|
+
const :extras, T::Array[String]
|
|
21
|
+
const :requirement_type, T.nilable(String), default: nil
|
|
22
|
+
|
|
23
|
+
sig { params(result: Object).returns(T::Array[PepDependency]) }
|
|
24
|
+
def self.from_helper_result(result)
|
|
25
|
+
PyprojectValueParser.array(result, "PEP dependency result").map do |value|
|
|
26
|
+
from_object(value)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(value: Object).returns(PepDependency) }
|
|
31
|
+
def self.from_object(value)
|
|
32
|
+
hash = PyprojectValueParser.object_hash(value, "PEP dependency")
|
|
33
|
+
new(
|
|
34
|
+
name: PyprojectValueParser.string(hash["name"], "PEP dependency name"),
|
|
35
|
+
version: PyprojectValueParser.optional_string(hash["version"], "PEP dependency version"),
|
|
36
|
+
markers: PyprojectValueParser.optional_string(hash["markers"], "PEP dependency markers"),
|
|
37
|
+
file: PyprojectValueParser.string(hash["file"], "PEP dependency file"),
|
|
38
|
+
requirement: PyprojectValueParser.optional_string(hash["requirement"], "PEP dependency requirement"),
|
|
39
|
+
source_requirement: PyprojectValueParser.optional_string(
|
|
40
|
+
hash["source_requirement"],
|
|
41
|
+
"PEP dependency source_requirement"
|
|
42
|
+
),
|
|
43
|
+
extras: PyprojectValueParser.string_array(hash["extras"], "PEP dependency extras"),
|
|
44
|
+
requirement_type: PyprojectValueParser.optional_string(
|
|
45
|
+
hash["requirement_type"],
|
|
46
|
+
"PEP dependency requirement_type"
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/python/file_parser"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Python
|
|
13
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
14
|
+
class PoetryLock
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
class Package < T::ImmutableStruct
|
|
18
|
+
extend T::Sig
|
|
19
|
+
|
|
20
|
+
const :name, String
|
|
21
|
+
const :version, String
|
|
22
|
+
const :source_type, T.nilable(String), default: nil
|
|
23
|
+
|
|
24
|
+
sig { params(value: Object).returns(Package) }
|
|
25
|
+
def self.from_object(value)
|
|
26
|
+
hash = PyprojectValueParser.object_hash(value, "Poetry lock package")
|
|
27
|
+
source = hash["source"]
|
|
28
|
+
source_hash =
|
|
29
|
+
if source.nil?
|
|
30
|
+
nil
|
|
31
|
+
else
|
|
32
|
+
PyprojectValueParser.object_hash(source, "Poetry lock package source")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
new(
|
|
36
|
+
name: PyprojectValueParser.string(hash["name"], "Poetry lock package name"),
|
|
37
|
+
version: PyprojectValueParser.string(hash["version"], "Poetry lock package version"),
|
|
38
|
+
source_type: PyprojectValueParser.optional_string(
|
|
39
|
+
source_hash&.[]("type"),
|
|
40
|
+
"Poetry lock package source type"
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { returns(T::Hash[Symbol, T.nilable(String)]) }
|
|
46
|
+
def to_h
|
|
47
|
+
{
|
|
48
|
+
name: name,
|
|
49
|
+
version: version,
|
|
50
|
+
source_type: source_type
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(T::Array[Package]) }
|
|
56
|
+
attr_reader :packages
|
|
57
|
+
|
|
58
|
+
sig { params(packages: T::Array[Package]).void }
|
|
59
|
+
def initialize(packages)
|
|
60
|
+
@packages = packages
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(file: Dependabot::DependencyFile).returns(PoetryLock) }
|
|
64
|
+
def self.from_file(file)
|
|
65
|
+
parsed = T.cast(TomlRB.parse(T.must(file.content)), Object)
|
|
66
|
+
document = PyprojectValueParser.object_hash(parsed, "poetry.lock")
|
|
67
|
+
value = document["package"]
|
|
68
|
+
packages =
|
|
69
|
+
if value.nil?
|
|
70
|
+
[]
|
|
71
|
+
else
|
|
72
|
+
PyprojectValueParser.array(value, "poetry.lock package").map do |package|
|
|
73
|
+
Package.from_object(package)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
new(packages)
|
|
77
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
78
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig do
|
|
82
|
+
params(
|
|
83
|
+
name: String,
|
|
84
|
+
_normalizer: T.proc.params(name: String).returns(String)
|
|
85
|
+
)
|
|
86
|
+
.returns(T.nilable(String))
|
|
87
|
+
end
|
|
88
|
+
def version_for(name, &_normalizer)
|
|
89
|
+
normalized_name = yield(name)
|
|
90
|
+
packages.find { |package| yield(package.name) == normalized_name }&.version
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/dependency_file"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/python/file_parser"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Python
|
|
13
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
14
|
+
module PyprojectValueParser
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
ObjectHash = T.type_alias { T::Hash[String, Object] }
|
|
18
|
+
PoetryRequirement = T.type_alias { T.any(String, ObjectHash) }
|
|
19
|
+
PoetryDependencyMap = T.type_alias { T::Hash[String, T::Array[PoetryRequirement]] }
|
|
20
|
+
|
|
21
|
+
sig { params(value: Object, context: String).returns(ObjectHash) }
|
|
22
|
+
def self.object_hash(value, context)
|
|
23
|
+
raise TypeError, "#{context} must be an object" unless value.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
result = T.let({}, ObjectHash)
|
|
26
|
+
value.each do |raw_key, raw_value|
|
|
27
|
+
key = T.cast(raw_key, Object)
|
|
28
|
+
raise TypeError, "#{context} keys must be strings" unless key.is_a?(String)
|
|
29
|
+
|
|
30
|
+
result[key] = T.cast(raw_value, Object)
|
|
31
|
+
end
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { params(value: Object, context: String).returns(T::Array[Object]) }
|
|
36
|
+
def self.array(value, context)
|
|
37
|
+
raise TypeError, "#{context} must be an array" unless value.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
value.map { |item| T.cast(item, Object) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(value: Object, context: String).returns(String) }
|
|
43
|
+
def self.string(value, context)
|
|
44
|
+
return value if value.is_a?(String)
|
|
45
|
+
|
|
46
|
+
raise TypeError, "#{context} must be a string"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { params(value: T.nilable(Object), context: String).returns(T.nilable(String)) }
|
|
50
|
+
def self.optional_string(value, context)
|
|
51
|
+
return if value.nil?
|
|
52
|
+
|
|
53
|
+
string(value, context)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(value: Object, context: String).returns(T::Array[String]) }
|
|
57
|
+
def self.string_array(value, context)
|
|
58
|
+
array(value, context).map do |item|
|
|
59
|
+
raise TypeError, "#{context} must contain only strings" unless item.is_a?(String)
|
|
60
|
+
|
|
61
|
+
item
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(value: Object, name: String).returns(T::Array[PoetryRequirement]) }
|
|
66
|
+
def self.poetry_requirements(value, name)
|
|
67
|
+
flattened = value.is_a?(Array) ? value.flatten : [value]
|
|
68
|
+
flattened.compact.map do |item|
|
|
69
|
+
item = T.cast(item, Object)
|
|
70
|
+
case item
|
|
71
|
+
when String
|
|
72
|
+
item
|
|
73
|
+
when Hash
|
|
74
|
+
object_hash(item, "Poetry dependency #{name}")
|
|
75
|
+
else
|
|
76
|
+
raise TypeError, "Poetry dependency #{name} must be a string, object, or array"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(value: Object, context: String).returns(PoetryDependencyMap) }
|
|
82
|
+
def self.poetry_dependency_map(value, context)
|
|
83
|
+
object_hash(value, context).to_h do |name, requirement|
|
|
84
|
+
[name, poetry_requirements(requirement, name)]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
private_constant :PyprojectValueParser
|
|
89
|
+
|
|
90
|
+
class PyprojectDocument
|
|
91
|
+
extend T::Sig
|
|
92
|
+
|
|
93
|
+
PoetryRequirement = T.type_alias { PyprojectValueParser::PoetryRequirement }
|
|
94
|
+
PoetryDependencyMap = T.type_alias { PyprojectValueParser::PoetryDependencyMap }
|
|
95
|
+
|
|
96
|
+
class PoetrySource < T::ImmutableStruct
|
|
97
|
+
const :name, String
|
|
98
|
+
const :url, T.nilable(String), default: nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(data: PyprojectValueParser::ObjectHash).void }
|
|
102
|
+
def initialize(data)
|
|
103
|
+
@data = data
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
sig { params(file: Dependabot::DependencyFile).returns(PyprojectDocument) }
|
|
107
|
+
def self.from_file(file)
|
|
108
|
+
content = T.must(file.content)
|
|
109
|
+
parsed = T.cast(TomlRB.parse(content), Object)
|
|
110
|
+
new(PyprojectValueParser.object_hash(parsed, "pyproject.toml"))
|
|
111
|
+
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
112
|
+
raise Dependabot::DependencyFileNotParseable, file.path
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { returns(T::Boolean) }
|
|
116
|
+
def poetry?
|
|
117
|
+
!poetry_root.nil?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
sig { returns(T::Boolean) }
|
|
121
|
+
def pep621?
|
|
122
|
+
project = section(@data, "project", "project")
|
|
123
|
+
build_system = section(@data, "build-system", "build-system")
|
|
124
|
+
|
|
125
|
+
!project&.[]("dependencies").nil? ||
|
|
126
|
+
!project&.[]("optional-dependencies").nil? ||
|
|
127
|
+
!build_system&.[]("requires").nil?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
sig { returns(T::Boolean) }
|
|
131
|
+
def pep735?
|
|
132
|
+
@data.key?("dependency-groups")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
sig { params(type: String).returns(PoetryDependencyMap) }
|
|
136
|
+
def poetry_dependencies(type)
|
|
137
|
+
root = poetry_root
|
|
138
|
+
return {} unless root
|
|
139
|
+
|
|
140
|
+
value = root[type]
|
|
141
|
+
return {} if value.nil?
|
|
142
|
+
|
|
143
|
+
PyprojectValueParser.poetry_dependency_map(value, "tool.poetry.#{type}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { returns(T::Hash[String, PoetryDependencyMap]) }
|
|
147
|
+
def poetry_groups
|
|
148
|
+
root = poetry_root
|
|
149
|
+
return {} unless root
|
|
150
|
+
|
|
151
|
+
raw_groups = root["group"]
|
|
152
|
+
return {} if raw_groups.nil?
|
|
153
|
+
|
|
154
|
+
parsed_groups = PyprojectValueParser.object_hash(raw_groups, "tool.poetry.group")
|
|
155
|
+
parsed_groups.each_with_object({}) do |(name, value), groups|
|
|
156
|
+
group = PyprojectValueParser.object_hash(value, "tool.poetry.group.#{name}")
|
|
157
|
+
dependencies = group["dependencies"]
|
|
158
|
+
next if dependencies.nil?
|
|
159
|
+
|
|
160
|
+
groups[name] = PyprojectValueParser.poetry_dependency_map(
|
|
161
|
+
dependencies,
|
|
162
|
+
"tool.poetry.group.#{name}.dependencies"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
sig { returns(T::Array[String]) }
|
|
168
|
+
def dynamic_fields
|
|
169
|
+
project = section(@data, "project", "project")
|
|
170
|
+
value = project&.[]("dynamic")
|
|
171
|
+
return [] if value.nil?
|
|
172
|
+
|
|
173
|
+
PyprojectValueParser.string_array(value, "project.dynamic")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
177
|
+
def optional_dependency_group?(name)
|
|
178
|
+
project = section(@data, "project", "project")
|
|
179
|
+
value = project&.[]("optional-dependencies")
|
|
180
|
+
return false if value.nil?
|
|
181
|
+
|
|
182
|
+
PyprojectValueParser.object_hash(value, "project.optional-dependencies").key?(name)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
sig { params(name: String).returns(T.nilable(PoetrySource)) }
|
|
186
|
+
def poetry_source(name)
|
|
187
|
+
poetry_sources.find { |source| source.name == name }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
sig { params(key: String).returns(T::Array[String]) }
|
|
191
|
+
def workspace_globs(key)
|
|
192
|
+
tool = section(@data, "tool", "tool")
|
|
193
|
+
uv = tool && section(tool, "uv", "tool.uv")
|
|
194
|
+
workspace = uv && section(uv, "workspace", "tool.uv.workspace")
|
|
195
|
+
value = workspace&.[](key)
|
|
196
|
+
return [] if value.nil?
|
|
197
|
+
|
|
198
|
+
PyprojectValueParser.string_array(value, "tool.uv.workspace.#{key}")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
sig { returns(T.nilable(PyprojectValueParser::ObjectHash)) }
|
|
204
|
+
def poetry_root
|
|
205
|
+
tool = section(@data, "tool", "tool")
|
|
206
|
+
tool && section(tool, "poetry", "tool.poetry")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
sig { returns(T::Array[PoetrySource]) }
|
|
210
|
+
def poetry_sources
|
|
211
|
+
root = poetry_root
|
|
212
|
+
return [] unless root
|
|
213
|
+
|
|
214
|
+
value = root["source"]
|
|
215
|
+
return [] if value.nil?
|
|
216
|
+
|
|
217
|
+
PyprojectValueParser.array(value, "tool.poetry.source").map do |source|
|
|
218
|
+
source = PyprojectValueParser.object_hash(source, "tool.poetry.source entry")
|
|
219
|
+
PoetrySource.new(
|
|
220
|
+
name: PyprojectValueParser.string(source["name"], "Poetry source name"),
|
|
221
|
+
url: PyprojectValueParser.optional_string(source["url"], "Poetry source url")
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
sig do
|
|
227
|
+
params(
|
|
228
|
+
hash: PyprojectValueParser::ObjectHash,
|
|
229
|
+
key: String,
|
|
230
|
+
context: String
|
|
231
|
+
).returns(T.nilable(PyprojectValueParser::ObjectHash))
|
|
232
|
+
end
|
|
233
|
+
def section(hash, key, context)
|
|
234
|
+
value = hash[key]
|
|
235
|
+
return if value.nil?
|
|
236
|
+
|
|
237
|
+
PyprojectValueParser.object_hash(value, context)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# typed:
|
|
1
|
+
# typed: strong
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "toml-rb"
|
|
@@ -25,7 +25,8 @@ module Dependabot
|
|
|
25
25
|
sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
|
|
26
26
|
def initialize(dependency_files:)
|
|
27
27
|
@dependency_files = dependency_files
|
|
28
|
-
@
|
|
28
|
+
@pyproject_document = T.let(nil, T.nilable(PyprojectDocument))
|
|
29
|
+
@poetry_lock_document = T.let(nil, T.nilable(PoetryLock))
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
@@ -63,16 +64,11 @@ module Dependabot
|
|
|
63
64
|
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
|
64
65
|
|
|
65
66
|
POETRY_DEPENDENCY_TYPES.each do |type|
|
|
66
|
-
|
|
67
|
-
dependencies += parse_poetry_dependency_group(type, deps_hash)
|
|
67
|
+
dependencies += parse_poetry_dependency_group(type, pyproject_document.poetry_dependencies(type))
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
deps = group_spec["dependencies"]
|
|
73
|
-
next unless deps
|
|
74
|
-
|
|
75
|
-
dependencies += parse_poetry_dependency_group(group, deps)
|
|
70
|
+
pyproject_document.poetry_groups.each do |group, dependencies_by_name|
|
|
71
|
+
dependencies += parse_poetry_dependency_group(group, dependencies_by_name)
|
|
76
72
|
end
|
|
77
73
|
dependencies
|
|
78
74
|
end
|
|
@@ -92,17 +88,17 @@ module Dependabot
|
|
|
92
88
|
|
|
93
89
|
dependencies <<
|
|
94
90
|
Dependency.new(
|
|
95
|
-
name: normalise(dep
|
|
96
|
-
version: dep
|
|
91
|
+
name: normalise(dep.name),
|
|
92
|
+
version: dep.version&.include?("*") ? nil : dep.version,
|
|
97
93
|
requirements: [{
|
|
98
|
-
requirement: dep
|
|
99
|
-
file: Pathname.new(dep
|
|
94
|
+
requirement: dep.requirement,
|
|
95
|
+
file: Pathname.new(dep.file).cleanpath.to_path,
|
|
100
96
|
source: nil,
|
|
101
|
-
groups: [dep
|
|
97
|
+
groups: [dep.requirement_type].compact
|
|
102
98
|
}],
|
|
103
99
|
package_manager: "pip",
|
|
104
|
-
metadata: extras_metadata(dep
|
|
105
|
-
source_requirement: dep
|
|
100
|
+
metadata: extras_metadata(dep.extras).merge(
|
|
101
|
+
source_requirement: dep.source_requirement
|
|
106
102
|
).compact
|
|
107
103
|
)
|
|
108
104
|
end
|
|
@@ -113,17 +109,16 @@ module Dependabot
|
|
|
113
109
|
sig do
|
|
114
110
|
params(
|
|
115
111
|
type: String,
|
|
116
|
-
deps_hash:
|
|
112
|
+
deps_hash: PyprojectDocument::PoetryDependencyMap
|
|
117
113
|
).returns(Dependabot::FileParsers::Base::DependencySet)
|
|
118
114
|
end
|
|
119
115
|
def parse_poetry_dependency_group(type, deps_hash)
|
|
120
116
|
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
|
121
|
-
return dependencies if deps_hash.nil?
|
|
122
117
|
|
|
123
|
-
deps_hash.each do |name,
|
|
118
|
+
deps_hash.each do |name, requirements_data|
|
|
124
119
|
next if normalise(name) == "python"
|
|
125
120
|
|
|
126
|
-
requirements = parse_requirements_from(
|
|
121
|
+
requirements = parse_requirements_from(requirements_data, type)
|
|
127
122
|
next if requirements.empty?
|
|
128
123
|
|
|
129
124
|
dependencies << Dependency.new(
|
|
@@ -151,102 +146,108 @@ module Dependabot
|
|
|
151
146
|
{ extras: extras.join(",") }
|
|
152
147
|
end
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
[
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
requirement: requirement,
|
|
181
|
-
file: T.must(pyproject).name,
|
|
182
|
-
source: nil,
|
|
183
|
-
groups: [type]
|
|
184
|
-
}
|
|
185
|
-
else
|
|
186
|
-
check_requirements(requirement)
|
|
187
|
-
# String sources are registry name references (e.g., "custom") that reference
|
|
188
|
-
# [[tool.poetry.source]] definitions. Resolve them to proper hashes.
|
|
189
|
-
source_value = requirement.fetch("source", nil)
|
|
190
|
-
source = resolve_source(source_value)
|
|
191
|
-
{
|
|
192
|
-
requirement: requirement["version"],
|
|
193
|
-
file: T.must(pyproject).name,
|
|
194
|
-
source: source,
|
|
195
|
-
groups: [type]
|
|
196
|
-
}
|
|
197
|
-
end
|
|
149
|
+
sig do
|
|
150
|
+
params(
|
|
151
|
+
requirements_data: T::Array[PyprojectDocument::PoetryRequirement],
|
|
152
|
+
type: String
|
|
153
|
+
).returns(T::Array[Dependabot::Dependency::RequirementInput])
|
|
154
|
+
end
|
|
155
|
+
def parse_requirements_from(requirements_data, type)
|
|
156
|
+
requirements_data.filter_map { |requirement| poetry_requirement(requirement, type) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig do
|
|
160
|
+
params(
|
|
161
|
+
requirement: PyprojectDocument::PoetryRequirement,
|
|
162
|
+
type: String
|
|
163
|
+
).returns(T.nilable(Dependabot::Dependency::RequirementInput))
|
|
164
|
+
end
|
|
165
|
+
def poetry_requirement(requirement, type)
|
|
166
|
+
if requirement.is_a?(String)
|
|
167
|
+
check_requirements(requirement)
|
|
168
|
+
return {
|
|
169
|
+
requirement: requirement,
|
|
170
|
+
file: T.must(pyproject).name,
|
|
171
|
+
source: nil,
|
|
172
|
+
groups: [type]
|
|
173
|
+
}
|
|
198
174
|
end
|
|
175
|
+
|
|
176
|
+
return if UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
|
|
177
|
+
|
|
178
|
+
git = requirement_string(requirement, "git")
|
|
179
|
+
tag = requirement_string(requirement, "tag")
|
|
180
|
+
return if git && !tag
|
|
181
|
+
|
|
182
|
+
return git_requirement(git, tag, type) if git && tag
|
|
183
|
+
|
|
184
|
+
check_requirements(requirement)
|
|
185
|
+
{
|
|
186
|
+
requirement: requirement_string(requirement, "version"),
|
|
187
|
+
file: T.must(pyproject).name,
|
|
188
|
+
source: resolve_source(requirement["source"]),
|
|
189
|
+
groups: [type]
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
sig { params(git: String, tag: String, type: String).returns(Dependabot::Dependency::RequirementInput) }
|
|
194
|
+
def git_requirement(git, tag, type)
|
|
195
|
+
{
|
|
196
|
+
requirement: nil,
|
|
197
|
+
file: T.must(pyproject).name,
|
|
198
|
+
source: {
|
|
199
|
+
type: "git",
|
|
200
|
+
url: git,
|
|
201
|
+
ref: tag,
|
|
202
|
+
branch: nil
|
|
203
|
+
},
|
|
204
|
+
groups: [type]
|
|
205
|
+
}
|
|
199
206
|
end
|
|
200
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
201
207
|
|
|
202
|
-
sig { returns(T
|
|
208
|
+
sig { returns(T::Boolean) }
|
|
203
209
|
def using_poetry?
|
|
204
|
-
|
|
210
|
+
pyproject_document.poetry?
|
|
205
211
|
end
|
|
206
212
|
|
|
207
213
|
sig { returns(T::Boolean) }
|
|
208
214
|
def using_pep621?
|
|
209
|
-
|
|
210
|
-
!parsed_pyproject.dig("project", "optional-dependencies").nil? ||
|
|
211
|
-
!parsed_pyproject.dig("build-system", "requires").nil?
|
|
215
|
+
pyproject_document.pep621?
|
|
212
216
|
end
|
|
213
217
|
|
|
214
218
|
sig { returns(T::Boolean) }
|
|
215
219
|
def using_pep735?
|
|
216
|
-
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
sig { returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
220
|
-
def poetry_root
|
|
221
|
-
parsed_pyproject.dig("tool", "poetry")
|
|
220
|
+
pyproject_document.pep735?
|
|
222
221
|
end
|
|
223
222
|
|
|
224
|
-
sig { returns(T
|
|
223
|
+
sig { returns(T::Boolean) }
|
|
225
224
|
def using_pdm?
|
|
226
|
-
using_pep621? && pdm_lock
|
|
225
|
+
using_pep621? && !pdm_lock.nil?
|
|
227
226
|
end
|
|
228
227
|
|
|
229
228
|
sig { returns(T::Array[String]) }
|
|
230
229
|
def dynamic_fields
|
|
231
|
-
|
|
230
|
+
pyproject_document.dynamic_fields
|
|
232
231
|
end
|
|
233
232
|
|
|
234
|
-
sig { params(dep:
|
|
233
|
+
sig { params(dep: PepDependency).returns(T::Boolean) }
|
|
235
234
|
def skip_pep621_dep?(dep)
|
|
236
235
|
# If a requirement has a `<` or `<=` marker then updating it is
|
|
237
236
|
# probably blocked. Ignore it.
|
|
238
|
-
return true if dep
|
|
237
|
+
return true if dep.markers&.include?("<")
|
|
239
238
|
|
|
240
239
|
# If no requirement, don't add it
|
|
241
|
-
|
|
240
|
+
requirement = dep.requirement
|
|
241
|
+
raise TypeError, "Python PEP dependency requirement must be a string" unless requirement
|
|
242
|
+
return true if requirement.empty?
|
|
242
243
|
|
|
243
244
|
# Skip build-system.requires dependencies when using Poetry
|
|
244
245
|
# Poetry manages its own build system dependencies
|
|
245
|
-
return true if using_poetry? && dep
|
|
246
|
+
return true if using_poetry? && dep.requirement_type == "build-system.requires"
|
|
246
247
|
|
|
247
248
|
# When dependencies or optional-dependencies are listed in project.dynamic,
|
|
248
249
|
# they are managed by the build backend (e.g. Poetry) — skip the PEP 621 path
|
|
249
|
-
dynamic_pep621_dep?(dep
|
|
250
|
+
dynamic_pep621_dep?(dep.requirement_type)
|
|
250
251
|
end
|
|
251
252
|
|
|
252
253
|
sig { params(requirement_type: T.nilable(String)).returns(T::Boolean) }
|
|
@@ -256,7 +257,7 @@ module Dependabot
|
|
|
256
257
|
|
|
257
258
|
if requirement_type == "dependencies"
|
|
258
259
|
dynamic_fields.include?("dependencies")
|
|
259
|
-
elsif
|
|
260
|
+
elsif pyproject_document.optional_dependency_group?(requirement_type)
|
|
260
261
|
dynamic_fields.include?("optional-dependencies")
|
|
261
262
|
else
|
|
262
263
|
false
|
|
@@ -271,15 +272,15 @@ module Dependabot
|
|
|
271
272
|
dependencies = Dependabot::FileParsers::Base::DependencySet.new
|
|
272
273
|
|
|
273
274
|
source_types = %w(directory git url)
|
|
274
|
-
|
|
275
|
-
next if source_types.include?(
|
|
275
|
+
poetry_lock_document.packages.each do |package|
|
|
276
|
+
next if source_types.include?(package.source_type)
|
|
276
277
|
|
|
277
|
-
name = normalise(
|
|
278
|
+
name = normalise(package.name)
|
|
278
279
|
|
|
279
280
|
dependencies <<
|
|
280
281
|
Dependency.new(
|
|
281
282
|
name: name,
|
|
282
|
-
version:
|
|
283
|
+
version: package.version,
|
|
283
284
|
requirements: [],
|
|
284
285
|
package_manager: "pip",
|
|
285
286
|
subdependency_metadata: [{
|
|
@@ -302,8 +303,10 @@ module Dependabot
|
|
|
302
303
|
sig { returns(T::Array[T.nilable(String)]) }
|
|
303
304
|
def parse_production_dependency_names
|
|
304
305
|
SharedHelpers.in_a_temporary_directory do
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
pyproject_file = T.must(pyproject)
|
|
307
|
+
lockfile_file = T.must(lockfile)
|
|
308
|
+
File.write(pyproject_file.name, pyproject_file.content)
|
|
309
|
+
File.write(lockfile_file.name, lockfile_file.content)
|
|
307
310
|
|
|
308
311
|
begin
|
|
309
312
|
output = SharedHelpers.run_shell_command("pyenv exec poetry show --only main")
|
|
@@ -322,18 +325,19 @@ module Dependabot
|
|
|
322
325
|
end
|
|
323
326
|
end
|
|
324
327
|
|
|
325
|
-
sig { params(dep_name: String).returns(T.
|
|
328
|
+
sig { params(dep_name: String).returns(T.nilable(String)) }
|
|
326
329
|
def version_from_lockfile(dep_name)
|
|
327
|
-
return unless
|
|
330
|
+
return unless poetry_lock
|
|
328
331
|
|
|
329
|
-
|
|
330
|
-
.find { |p| normalise(p.fetch("name")) == normalise(dep_name) }
|
|
331
|
-
&.fetch("version", nil)
|
|
332
|
+
poetry_lock_document.version_for(dep_name) { |name| normalise(name) }
|
|
332
333
|
end
|
|
333
334
|
|
|
334
|
-
sig
|
|
335
|
+
sig do
|
|
336
|
+
params(req: PyprojectDocument::PoetryRequirement)
|
|
337
|
+
.returns(T::Array[Dependabot::Python::Requirement])
|
|
338
|
+
end
|
|
335
339
|
def check_requirements(req)
|
|
336
|
-
requirement = req.is_a?(String) ? req : req
|
|
340
|
+
requirement = req.is_a?(String) ? req : requirement_string(req, "version")
|
|
337
341
|
Python::Requirement.requirements_array(requirement)
|
|
338
342
|
rescue Gem::Requirement::BadRequirementError => e
|
|
339
343
|
raise Dependabot::DependencyFileNotEvaluatable, e.message
|
|
@@ -344,21 +348,23 @@ module Dependabot
|
|
|
344
348
|
NameNormaliser.normalise(name)
|
|
345
349
|
end
|
|
346
350
|
|
|
347
|
-
sig
|
|
351
|
+
sig do
|
|
352
|
+
params(source_value: T.nilable(Object))
|
|
353
|
+
.returns(T.nilable(Dependabot::DependencyRequirement::Source))
|
|
354
|
+
end
|
|
348
355
|
def resolve_source(source_value)
|
|
349
356
|
# Return nil if no source specified
|
|
350
357
|
return nil if source_value.nil?
|
|
351
358
|
|
|
352
359
|
# If already a hash, return as-is (handles git sources)
|
|
353
|
-
return source_value if source_value.is_a?(Hash)
|
|
360
|
+
return PyprojectValueParser.object_hash(source_value, "Poetry dependency source") if source_value.is_a?(Hash)
|
|
354
361
|
|
|
355
362
|
# String sources are references to [[tool.poetry.source]] definitions
|
|
356
363
|
# Look up the source definition and create a hash
|
|
357
364
|
return nil unless source_value.is_a?(String)
|
|
358
365
|
|
|
359
366
|
source_name = source_value
|
|
360
|
-
|
|
361
|
-
source_def = poetry_sources.find { |s| s["name"] == source_name }
|
|
367
|
+
source_def = pyproject_document.poetry_source(source_name)
|
|
362
368
|
|
|
363
369
|
# If source definition not found, return nil
|
|
364
370
|
return nil unless source_def
|
|
@@ -367,26 +373,19 @@ module Dependabot
|
|
|
367
373
|
# Use "registry" as the type since these are package index sources
|
|
368
374
|
{
|
|
369
375
|
type: "registry",
|
|
370
|
-
url: source_def
|
|
376
|
+
url: source_def.url,
|
|
371
377
|
name: source_name
|
|
372
378
|
}
|
|
373
379
|
end
|
|
374
380
|
|
|
375
|
-
sig { returns(
|
|
376
|
-
def
|
|
377
|
-
@
|
|
378
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
379
|
-
raise Dependabot::DependencyFileNotParseable, T.must(pyproject).path
|
|
381
|
+
sig { returns(PyprojectDocument) }
|
|
382
|
+
def pyproject_document
|
|
383
|
+
@pyproject_document ||= PyprojectDocument.from_file(T.must(pyproject))
|
|
380
384
|
end
|
|
381
385
|
|
|
382
|
-
sig { returns(
|
|
383
|
-
def
|
|
384
|
-
@
|
|
385
|
-
TomlRB.parse(T.must(poetry_lock).content),
|
|
386
|
-
T.nilable(T::Hash[String, T.untyped])
|
|
387
|
-
)
|
|
388
|
-
rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
|
|
389
|
-
raise Dependabot::DependencyFileNotParseable, T.must(poetry_lock).path
|
|
386
|
+
sig { returns(PoetryLock) }
|
|
387
|
+
def poetry_lock_document
|
|
388
|
+
@poetry_lock_document ||= PoetryLock.from_file(T.must(poetry_lock))
|
|
390
389
|
end
|
|
391
390
|
|
|
392
391
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
@@ -397,20 +396,22 @@ module Dependabot
|
|
|
397
396
|
)
|
|
398
397
|
end
|
|
399
398
|
|
|
400
|
-
sig { returns(T.
|
|
399
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
401
400
|
def lockfile
|
|
402
401
|
poetry_lock
|
|
403
402
|
end
|
|
404
403
|
|
|
405
|
-
sig { returns(T
|
|
404
|
+
sig { returns(T::Array[PepDependency]) }
|
|
406
405
|
def parse_pep621_pep735_dependencies
|
|
407
406
|
SharedHelpers.in_a_temporary_directory do
|
|
408
407
|
write_temporary_pyproject
|
|
409
408
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
409
|
+
PepDependency.from_helper_result(
|
|
410
|
+
SharedHelpers.run_helper_subprocess(
|
|
411
|
+
command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
|
|
412
|
+
function: "parse_pep621_pep735_dependencies",
|
|
413
|
+
args: [T.must(pyproject).name]
|
|
414
|
+
)
|
|
414
415
|
)
|
|
415
416
|
end
|
|
416
417
|
end
|
|
@@ -422,11 +423,6 @@ module Dependabot
|
|
|
422
423
|
File.write(path, T.must(pyproject).content)
|
|
423
424
|
end
|
|
424
425
|
|
|
425
|
-
sig { returns(T.untyped) }
|
|
426
|
-
def parsed_lockfile
|
|
427
|
-
parsed_poetry_lock if poetry_lock
|
|
428
|
-
end
|
|
429
|
-
|
|
430
426
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
431
427
|
def poetry_lock
|
|
432
428
|
@poetry_lock ||= T.let(
|
|
@@ -435,6 +431,16 @@ module Dependabot
|
|
|
435
431
|
)
|
|
436
432
|
end
|
|
437
433
|
|
|
434
|
+
sig do
|
|
435
|
+
params(
|
|
436
|
+
requirement: PyprojectValueParser::ObjectHash,
|
|
437
|
+
key: String
|
|
438
|
+
).returns(T.nilable(String))
|
|
439
|
+
end
|
|
440
|
+
def requirement_string(requirement, key)
|
|
441
|
+
PyprojectValueParser.optional_string(requirement[key], "Poetry dependency #{key}")
|
|
442
|
+
end
|
|
443
|
+
|
|
438
444
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
439
445
|
def pdm_lock
|
|
440
446
|
@pdm_lock ||= T.let(
|
|
@@ -21,6 +21,9 @@ module Dependabot
|
|
|
21
21
|
class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/ClassLength
|
|
22
22
|
extend T::Sig
|
|
23
23
|
|
|
24
|
+
require_relative "file_parser/pyproject_document"
|
|
25
|
+
require_relative "file_parser/poetry_lock"
|
|
26
|
+
require_relative "file_parser/pep_dependency"
|
|
24
27
|
require_relative "file_parser/pipfile_files_parser"
|
|
25
28
|
require_relative "file_parser/pyproject_files_parser"
|
|
26
29
|
require_relative "file_parser/setup_file_parser"
|
|
@@ -12,6 +12,7 @@ require "dependabot/python/name_normaliser"
|
|
|
12
12
|
require "dependabot/package/package_release"
|
|
13
13
|
require "dependabot/package/package_details"
|
|
14
14
|
require "dependabot/python/package/package_registry_finder"
|
|
15
|
+
require "dependabot/python/package/simple_api_parser"
|
|
15
16
|
|
|
16
17
|
# Stores metadata for a package, including all its available versions
|
|
17
18
|
module Dependabot
|
|
@@ -21,6 +22,7 @@ module Dependabot
|
|
|
21
22
|
CREDENTIALS_PASSWORD = "password"
|
|
22
23
|
|
|
23
24
|
APPLICATION_JSON = "application/json"
|
|
25
|
+
APPLICATION_PYPI_SIMPLE_JSON = "application/vnd.pypi.simple.v1+json"
|
|
24
26
|
APPLICATION_TEXT = "text/html"
|
|
25
27
|
CPYTHON = "cpython"
|
|
26
28
|
PYTHON = "python"
|
|
@@ -42,11 +44,7 @@ module Dependabot
|
|
|
42
44
|
credentials: T::Array[Dependabot::Credential]
|
|
43
45
|
).void
|
|
44
46
|
end
|
|
45
|
-
def initialize(
|
|
46
|
-
dependency:,
|
|
47
|
-
dependency_files:,
|
|
48
|
-
credentials:
|
|
49
|
-
)
|
|
47
|
+
def initialize(dependency:, dependency_files:, credentials:)
|
|
50
48
|
@dependency = dependency
|
|
51
49
|
@dependency_files = dependency_files
|
|
52
50
|
@credentials = credentials
|
|
@@ -70,7 +68,7 @@ module Dependabot
|
|
|
70
68
|
.flat_map do |index_url|
|
|
71
69
|
fetch_from_registry(index_url) || [] # Ensure it always returns an array
|
|
72
70
|
rescue Excon::Error::Timeout, Excon::Error::Socket
|
|
73
|
-
raise if
|
|
71
|
+
raise if main_pypi_index?(index_url)
|
|
74
72
|
|
|
75
73
|
raise PrivateSourceTimedOut, sanitized_url(index_url)
|
|
76
74
|
rescue URI::InvalidURIError
|
|
@@ -90,15 +88,46 @@ module Dependabot
|
|
|
90
88
|
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
|
91
89
|
end
|
|
92
90
|
def fetch_from_registry(index_url)
|
|
93
|
-
|
|
91
|
+
return fetch_from_simple_registry(index_url) unless main_pypi_index?(index_url)
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
begin
|
|
94
|
+
metadata = fetch_from_json_registry(index_url)
|
|
95
|
+
|
|
96
|
+
return metadata if metadata&.any?
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
|
|
99
|
+
fetch_from_html_registry(index_url)
|
|
100
|
+
rescue Dependabot::DependencyFileNotResolvable
|
|
101
|
+
raise
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
|
|
104
|
+
fetch_from_html_registry(index_url)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(index_url: String).returns(T::Array[Dependabot::Package::PackageRelease]) }
|
|
109
|
+
def fetch_from_simple_registry(index_url)
|
|
110
|
+
Dependabot.logger.info(
|
|
111
|
+
"Fetching release information from simple registry at #{sanitized_url(index_url)} for #{dependency.name}"
|
|
112
|
+
)
|
|
113
|
+
response = registry_response_for_dependency(index_url, accept: simple_api_accept)
|
|
114
|
+
project_url = response.path ? Excon::Utils.request_uri(response.data) : index_url + normalised_name + "/"
|
|
115
|
+
check_authentication_response(response, index_url)
|
|
116
|
+
|
|
117
|
+
version_releases = if simple_json_response?(response)
|
|
118
|
+
SimpleApiParser.new(
|
|
119
|
+
dependency: dependency,
|
|
120
|
+
project_url: project_url
|
|
121
|
+
).parse(response.body)
|
|
122
|
+
else
|
|
123
|
+
extract_release_details_json_from_html(response.body)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
format_version_releases(version_releases, preserve_distributions: simple_json_response?(response))
|
|
127
|
+
.sort_by(&:version).reverse
|
|
128
|
+
rescue JSON::ParserError
|
|
129
|
+
Dependabot.logger.warn("JSON parsing error for #{sanitized_url(index_url)}.")
|
|
130
|
+
[]
|
|
102
131
|
end
|
|
103
132
|
|
|
104
133
|
# Example JSON Response Format:
|
|
@@ -203,14 +232,8 @@ module Dependabot
|
|
|
203
232
|
Dependabot.logger.info(
|
|
204
233
|
"Fetching release information from html registry at #{sanitized_url(index_url)} for #{dependency.name}"
|
|
205
234
|
)
|
|
206
|
-
index_response = registry_response_for_dependency(index_url)
|
|
207
|
-
|
|
208
|
-
registry_index_response = registry_index_response(index_url)
|
|
209
|
-
|
|
210
|
-
if registry_index_response.status == 401 || registry_index_response.status == 403
|
|
211
|
-
raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
235
|
+
index_response = registry_response_for_dependency(index_url, accept: APPLICATION_TEXT)
|
|
236
|
+
check_authentication_response(index_response, index_url)
|
|
214
237
|
|
|
215
238
|
version_releases = extract_release_details_json_from_html(index_response.body)
|
|
216
239
|
releases = format_version_releases(version_releases)
|
|
@@ -218,6 +241,27 @@ module Dependabot
|
|
|
218
241
|
releases.sort_by(&:version).reverse
|
|
219
242
|
end
|
|
220
243
|
|
|
244
|
+
sig { params(response: Excon::Response).returns(T::Boolean) }
|
|
245
|
+
def simple_json_response?(response)
|
|
246
|
+
response.headers["Content-Type"].to_s.split(";", 2).first.to_s.strip.downcase == APPLICATION_PYPI_SIMPLE_JSON
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
sig { returns(String) }
|
|
250
|
+
def simple_api_accept
|
|
251
|
+
"application/vnd.pypi.simple.v1+json, " \
|
|
252
|
+
"application/vnd.pypi.simple.v1+html;q=0.2, text/html;q=0.01"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
sig { params(response: Excon::Response, index_url: String).void }
|
|
256
|
+
def check_authentication_response(response, index_url)
|
|
257
|
+
return unless [401, 403].include?(response.status)
|
|
258
|
+
|
|
259
|
+
index_response = registry_index_response(index_url)
|
|
260
|
+
return unless [401, 403].include?(index_response.status)
|
|
261
|
+
|
|
262
|
+
raise PrivateSourceAuthenticationFailure, sanitized_url(index_url)
|
|
263
|
+
end
|
|
264
|
+
|
|
221
265
|
sig do
|
|
222
266
|
params(html_body: String)
|
|
223
267
|
.returns(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]) # Returns JSON-like format
|
|
@@ -266,23 +310,17 @@ module Dependabot
|
|
|
266
310
|
|
|
267
311
|
sig do
|
|
268
312
|
params(
|
|
269
|
-
releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]])
|
|
313
|
+
releases_json: T.nilable(T::Hash[String, T::Array[T::Hash[String, T.untyped]]]),
|
|
314
|
+
preserve_distributions: T::Boolean
|
|
270
315
|
)
|
|
271
316
|
.returns(T::Array[Dependabot::Package::PackageRelease])
|
|
272
317
|
end
|
|
273
|
-
def format_version_releases(releases_json)
|
|
318
|
+
def format_version_releases(releases_json, preserve_distributions: false)
|
|
274
319
|
return [] unless releases_json
|
|
275
320
|
|
|
276
321
|
releases_json.each_with_object([]) do |(version, release_data_array), versions|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
next unless release_data
|
|
280
|
-
|
|
281
|
-
release = format_version_release(version, release_data)
|
|
282
|
-
|
|
283
|
-
next unless release
|
|
284
|
-
|
|
285
|
-
versions << release
|
|
322
|
+
release_data_array = [release_data_array.last].compact unless preserve_distributions
|
|
323
|
+
versions.concat(release_data_array.filter_map { |data| format_version_release(version, data) })
|
|
286
324
|
end
|
|
287
325
|
end
|
|
288
326
|
|
|
@@ -397,11 +435,11 @@ module Dependabot
|
|
|
397
435
|
)
|
|
398
436
|
end
|
|
399
437
|
|
|
400
|
-
sig { params(index_url: String).returns(Excon::Response) }
|
|
401
|
-
def registry_response_for_dependency(index_url)
|
|
438
|
+
sig { params(index_url: String, accept: String).returns(Excon::Response) }
|
|
439
|
+
def registry_response_for_dependency(index_url, accept:)
|
|
402
440
|
Dependabot::RegistryClient.get(
|
|
403
441
|
url: index_url + normalised_name + "/",
|
|
404
|
-
headers: { "Accept" =>
|
|
442
|
+
headers: { "Accept" => accept }
|
|
405
443
|
)
|
|
406
444
|
end
|
|
407
445
|
|
|
@@ -487,6 +525,14 @@ module Dependabot
|
|
|
487
525
|
index_url.sub(%r{//([^/@]+)@}, "//redacted@")
|
|
488
526
|
end
|
|
489
527
|
|
|
528
|
+
sig { params(index_url: String).returns(T::Boolean) }
|
|
529
|
+
def main_pypi_index?(index_url)
|
|
530
|
+
uri = URI.parse(index_url)
|
|
531
|
+
T.must(uri.scheme.to_s.casecmp("https")).zero? && uri.port == 443 &&
|
|
532
|
+
uri.path.to_s.split("/").reject(&:empty?) == ["simple"] &&
|
|
533
|
+
%w(pypi.org pypi.python.org).include?(uri.host.to_s.downcase)
|
|
534
|
+
end
|
|
535
|
+
|
|
490
536
|
sig { params(dep_name: String).returns(String) }
|
|
491
537
|
def remove_optional(dep_name)
|
|
492
538
|
# Remove any optional dependencies postfix, e.g., "pyvista[io] "
|
|
@@ -193,7 +193,7 @@ module Dependabot
|
|
|
193
193
|
|
|
194
194
|
sig { params(url: String).returns(String) }
|
|
195
195
|
def clean_check_and_remove_environment_variables(url)
|
|
196
|
-
url = url.strip
|
|
196
|
+
url = with_single_trailing_slash(url.strip)
|
|
197
197
|
|
|
198
198
|
return authed_base_url(url) unless url.match?(ENVIRONMENT_VARIABLE_REGEX)
|
|
199
199
|
|
|
@@ -203,7 +203,7 @@ module Dependabot
|
|
|
203
203
|
*config_variable_index_urls[:extra]
|
|
204
204
|
]
|
|
205
205
|
.compact
|
|
206
|
-
.map { |u| u.strip
|
|
206
|
+
.map { |u| with_single_trailing_slash(u.strip) }
|
|
207
207
|
|
|
208
208
|
regexp = url
|
|
209
209
|
.sub(%r{(?<=://).+@}, "")
|
|
@@ -226,7 +226,7 @@ module Dependabot
|
|
|
226
226
|
cred = credential_for(base_url)
|
|
227
227
|
return base_url unless cred
|
|
228
228
|
|
|
229
|
-
AuthedUrlBuilder.authed_url(credential: cred)
|
|
229
|
+
with_single_trailing_slash(AuthedUrlBuilder.authed_url(credential: cred))
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
sig { params(url: String).returns(T.nilable(Dependabot::Credential)) }
|
|
@@ -234,11 +234,18 @@ module Dependabot
|
|
|
234
234
|
credentials
|
|
235
235
|
.select { |c| c["type"] == "python_index" }
|
|
236
236
|
.find do |c|
|
|
237
|
-
cred_url = c.fetch("index-url")
|
|
237
|
+
cred_url = with_single_trailing_slash(c.fetch("index-url"))
|
|
238
238
|
cred_url.include?(url)
|
|
239
239
|
end
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
+
sig { params(url: String).returns(String) }
|
|
243
|
+
def with_single_trailing_slash(url)
|
|
244
|
+
normalized_url = url.dup
|
|
245
|
+
normalized_url.chop! while normalized_url.end_with?("/")
|
|
246
|
+
normalized_url + "/"
|
|
247
|
+
end
|
|
248
|
+
|
|
242
249
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
243
250
|
def pip_conf
|
|
244
251
|
dependency_files.find { |f| f.name == "pip.conf" }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "sorbet-runtime"
|
|
7
|
+
require "dependabot/errors"
|
|
8
|
+
require "dependabot/python/name_normaliser"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Python
|
|
12
|
+
module Package
|
|
13
|
+
class SimpleApiParser
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
ReleaseDetail = T.type_alias { T::Hash[String, T.nilable(T.any(String, T::Boolean))] }
|
|
17
|
+
|
|
18
|
+
sig { params(dependency: Dependabot::Dependency, project_url: String).void }
|
|
19
|
+
def initialize(dependency:, project_url:)
|
|
20
|
+
@dependency = dependency
|
|
21
|
+
@project_url = project_url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig do
|
|
25
|
+
params(json_body: String)
|
|
26
|
+
.returns(T::Hash[String, T::Array[ReleaseDetail]])
|
|
27
|
+
end
|
|
28
|
+
def parse(json_body)
|
|
29
|
+
data = JSON.parse(json_body)
|
|
30
|
+
validate_api_version!(data.dig("meta", "api-version").to_s)
|
|
31
|
+
|
|
32
|
+
data.fetch("files", []).each_with_object({}) do |file, releases|
|
|
33
|
+
filename = file["filename"]
|
|
34
|
+
next unless filename&.match?(name_regex)
|
|
35
|
+
|
|
36
|
+
version = version_from_filename(filename)
|
|
37
|
+
next unless dependency.version_class.correct?(version)
|
|
38
|
+
|
|
39
|
+
yanked = file["yanked"] || false
|
|
40
|
+
details = {
|
|
41
|
+
"version" => version,
|
|
42
|
+
"requires_python" => file["requires-python"],
|
|
43
|
+
"yanked" => yanked != false,
|
|
44
|
+
"yanked_reason" => yanked.is_a?(String) ? yanked : nil,
|
|
45
|
+
"upload_time" => file["upload-time"],
|
|
46
|
+
"url" => resolve_url(file["url"])
|
|
47
|
+
}
|
|
48
|
+
releases[version] ||= []
|
|
49
|
+
releases[version] << details
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
sig { returns(Dependabot::Dependency) }
|
|
56
|
+
attr_reader :dependency
|
|
57
|
+
|
|
58
|
+
sig { returns(String) }
|
|
59
|
+
attr_reader :project_url
|
|
60
|
+
|
|
61
|
+
sig { params(url: T.nilable(String)).returns(T.nilable(String)) }
|
|
62
|
+
def resolve_url(url)
|
|
63
|
+
return unless url
|
|
64
|
+
|
|
65
|
+
resolved_url = URI.join(project_url, url)
|
|
66
|
+
resolved_url.user = nil
|
|
67
|
+
resolved_url.password = nil
|
|
68
|
+
resolved_url.to_s
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { params(api_version: String).void }
|
|
72
|
+
def validate_api_version!(api_version)
|
|
73
|
+
return unless api_version.split(".").first.to_i > 1
|
|
74
|
+
|
|
75
|
+
raise Dependabot::DependencyFileNotResolvable, "Unsupported PEP 691 API version: #{api_version}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { returns(Regexp) }
|
|
79
|
+
def name_regex
|
|
80
|
+
parts = NameNormaliser.normalise(dependency.name).split(/[\s_.-]/).map { |name| Regexp.quote(name) }
|
|
81
|
+
/#{parts.join("[\s_.-]")}/i
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sig { params(filename: String).returns(T.nilable(String)) }
|
|
85
|
+
def version_from_filename(filename)
|
|
86
|
+
filename.strip.gsub(/#{name_regex}-/i, "").split(/-|\.tar\.|\.zip|\.whl/).first
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -249,8 +249,7 @@ module Dependabot
|
|
|
249
249
|
next if previously_fetched_files.map(&:name).include?(path)
|
|
250
250
|
next if file.name == path
|
|
251
251
|
|
|
252
|
-
if Dependabot::
|
|
253
|
-
!@exclude_paths.empty? && Dependabot::FileFiltering.exclude_path?(path, @exclude_paths)
|
|
252
|
+
if !@exclude_paths.empty? && Dependabot::FileFiltering.exclude_path?(path, @exclude_paths)
|
|
254
253
|
raise Dependabot::DependencyFileNotEvaluatable,
|
|
255
254
|
"Cannot process requirements: '#{file.name}' references excluded file '#{path}'. " \
|
|
256
255
|
"Please either remove the reference from '#{file.name}' " \
|
|
@@ -42,6 +42,22 @@ module Dependabot
|
|
|
42
42
|
cooldown.semver_minor_days.to_i.positive? ||
|
|
43
43
|
cooldown.semver_patch_days.to_i.positive?
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
sig do
|
|
47
|
+
params(language_version: T.nilable(T.any(String, Dependabot::Version)))
|
|
48
|
+
.returns(T.nilable(T::Array[Dependabot::Package::PackageRelease]))
|
|
49
|
+
end
|
|
50
|
+
def eligible_releases(language_version: nil)
|
|
51
|
+
releases = available_versions
|
|
52
|
+
return unless releases
|
|
53
|
+
|
|
54
|
+
releases = filter_yanked_versions(releases)
|
|
55
|
+
releases = filter_by_cooldown(releases)
|
|
56
|
+
releases = filter_unsupported_versions(releases, language_version)
|
|
57
|
+
releases = filter_prerelease_versions(releases)
|
|
58
|
+
releases = filter_ignored_versions(releases)
|
|
59
|
+
apply_post_fetch_latest_versions_filter(releases)
|
|
60
|
+
end
|
|
45
61
|
end
|
|
46
62
|
end
|
|
47
63
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-python
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.394.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dependabot
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.394.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.394.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -169,14 +169,14 @@ dependencies:
|
|
|
169
169
|
requirements:
|
|
170
170
|
- - "~>"
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: '
|
|
172
|
+
version: '1.1'
|
|
173
173
|
type: :development
|
|
174
174
|
prerelease: false
|
|
175
175
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
176
|
requirements:
|
|
177
177
|
- - "~>"
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
|
-
version: '
|
|
179
|
+
version: '1.1'
|
|
180
180
|
- !ruby/object:Gem::Dependency
|
|
181
181
|
name: turbo_tests
|
|
182
182
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -280,7 +280,10 @@ files:
|
|
|
280
280
|
- lib/dependabot/python/dependency_grapher/requirements_layers.rb
|
|
281
281
|
- lib/dependabot/python/file_fetcher.rb
|
|
282
282
|
- lib/dependabot/python/file_parser.rb
|
|
283
|
+
- lib/dependabot/python/file_parser/pep_dependency.rb
|
|
283
284
|
- lib/dependabot/python/file_parser/pipfile_files_parser.rb
|
|
285
|
+
- lib/dependabot/python/file_parser/poetry_lock.rb
|
|
286
|
+
- lib/dependabot/python/file_parser/pyproject_document.rb
|
|
284
287
|
- lib/dependabot/python/file_parser/pyproject_files_parser.rb
|
|
285
288
|
- lib/dependabot/python/file_parser/python_requirement_parser.rb
|
|
286
289
|
- lib/dependabot/python/file_parser/setup_file_parser.rb
|
|
@@ -302,6 +305,7 @@ files:
|
|
|
302
305
|
- lib/dependabot/python/native_helpers.rb
|
|
303
306
|
- lib/dependabot/python/package/package_details_fetcher.rb
|
|
304
307
|
- lib/dependabot/python/package/package_registry_finder.rb
|
|
308
|
+
- lib/dependabot/python/package/simple_api_parser.rb
|
|
305
309
|
- lib/dependabot/python/package_manager.rb
|
|
306
310
|
- lib/dependabot/python/pip_compile_file_matcher.rb
|
|
307
311
|
- lib/dependabot/python/pipenv_runner.rb
|
|
@@ -323,7 +327,7 @@ licenses:
|
|
|
323
327
|
- MIT
|
|
324
328
|
metadata:
|
|
325
329
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
326
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
330
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.394.0
|
|
327
331
|
rdoc_options: []
|
|
328
332
|
require_paths:
|
|
329
333
|
- lib
|