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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 242cdbd71bb6b0424ba7c88daf21d1839db3eb9d3f0beb2ad9e070f3d624cf77
4
- data.tar.gz: 40a2faa1353d5fd82480211fee5ac28840996cf483fdf3c884fb558702396f08
3
+ metadata.gz: 0d1b1deb24d07d3b15f0c1cb2a434bc4014459ff9e52173a6b1aa1608bed9bef
4
+ data.tar.gz: c2a82f5e34e3f06ecfd16ad0516aa925d7cea4d564b9436befaf50ea5eaeb05f
5
5
  SHA512:
6
- metadata.gz: 9a75d27db29ca5df37ea4e4962da3cbbfa0c05627044fd340d9b0b6f657108d44ff78bc40c206b2d0ac3a6d8387f3f0f30cd7dfeacafee122fd92b3e2d981954
7
- data.tar.gz: 7651c73fdab13d9f091cada4a7d7531842e90042825ef0052c67882e90958870a26fd3936ac213bcd69362d9ac3b9d64d422a34ea4f9646344468851c6969b7c
6
+ metadata.gz: c6f557bb606f5cdd0105035e2569dfb80410716ae0b6eb514c46d92837f4677107e2f22c309dd90fcd983c0b4a0d23a8ef596036f242560670de569b90a01b43
7
+ data.tar.gz: 7426a6a6748ef14ed637e82fa5757e5a11fc2c56fadb9e549b559a35e0bd831985978be092f6908068ae5cdd787d330ff95bc7e9b54dc0add588523a4a194d6d
@@ -1,4 +1,4 @@
1
- pip==26.1.2
1
+ pip==26.2.1
2
2
  pip-tools==7.6.1
3
3
  flake8==7.3.0
4
4
  hashin==1.0.5
@@ -10,4 +10,4 @@ pytest==9.1.1
10
10
  tomli==2.4.1
11
11
 
12
12
  # Some dependencies will only install if Cython is present
13
- Cython==3.2.9
13
+ Cython==3.3.0
@@ -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: strict
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
- @dynamic_fields = T.let(nil, T.nilable(T::Array[String]))
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
- deps_hash = T.must(poetry_root)[type] || {}
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
- groups = T.must(poetry_root)["group"] || {}
71
- groups.each do |group, group_spec|
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["name"]),
96
- version: dep["version"]&.include?("*") ? nil : dep["version"],
91
+ name: normalise(dep.name),
92
+ version: dep.version&.include?("*") ? nil : dep.version,
97
93
  requirements: [{
98
- requirement: dep["requirement"],
99
- file: Pathname.new(dep["file"]).cleanpath.to_path,
94
+ requirement: dep.requirement,
95
+ file: Pathname.new(dep.file).cleanpath.to_path,
100
96
  source: nil,
101
- groups: [dep["requirement_type"]].compact
97
+ groups: [dep.requirement_type].compact
102
98
  }],
103
99
  package_manager: "pip",
104
- metadata: extras_metadata(dep["extras"]).merge(
105
- source_requirement: dep["source_requirement"]
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: T.nilable(T::Hash[String, T.untyped])
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, req|
118
+ deps_hash.each do |name, requirements_data|
124
119
  next if normalise(name) == "python"
125
120
 
126
- requirements = parse_requirements_from(req, type)
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
- # @param req can be an Array, Hash or String that represents the constraints for a dependency
155
- sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
156
- # rubocop:disable Metrics/PerceivedComplexity
157
- def parse_requirements_from(req, type)
158
- [req].flatten.compact.filter_map do |requirement|
159
- # Skip unsupported dependency types (path, url), but allow git
160
- next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
161
- # Skip git dependencies without tags (e.g., with branch, rev)
162
- next if requirement.is_a?(Hash) && requirement["git"] && !requirement["tag"]
163
-
164
- # Handle git dependencies with tags
165
- if requirement.is_a?(Hash) && requirement["git"] && requirement["tag"]
166
- {
167
- requirement: nil,
168
- file: T.must(pyproject).name,
169
- source: {
170
- type: "git",
171
- url: requirement["git"],
172
- ref: requirement["tag"],
173
- branch: nil
174
- },
175
- groups: [type]
176
- }
177
- elsif requirement.is_a?(String)
178
- check_requirements(requirement)
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.nilable(T::Boolean)) }
208
+ sig { returns(T::Boolean) }
203
209
  def using_poetry?
204
- !poetry_root.nil?
210
+ pyproject_document.poetry?
205
211
  end
206
212
 
207
213
  sig { returns(T::Boolean) }
208
214
  def using_pep621?
209
- !parsed_pyproject.dig("project", "dependencies").nil? ||
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
- parsed_pyproject.key?("dependency-groups")
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.untyped) }
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
- @dynamic_fields ||= parsed_pyproject.dig("project", "dynamic") || []
230
+ pyproject_document.dynamic_fields
232
231
  end
233
232
 
234
- sig { params(dep: T::Hash[String, T.untyped]).returns(T::Boolean) }
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["markers"]&.include?("<")
237
+ return true if dep.markers&.include?("<")
239
238
 
240
239
  # If no requirement, don't add it
241
- return true if dep["requirement"].empty?
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["requirement_type"] == "build-system.requires"
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["requirement_type"])
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 parsed_pyproject.dig("project", "optional-dependencies")&.key?(requirement_type)
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
- parsed_lockfile.fetch("package", []).each do |details|
275
- next if source_types.include?(details.dig("source", "type"))
275
+ poetry_lock_document.packages.each do |package|
276
+ next if source_types.include?(package.source_type)
276
277
 
277
- name = normalise(details.fetch("name"))
278
+ name = normalise(package.name)
278
279
 
279
280
  dependencies <<
280
281
  Dependency.new(
281
282
  name: name,
282
- version: details.fetch("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
- File.write(T.must(pyproject).name, T.must(pyproject).content)
306
- File.write(lockfile.name, lockfile.content)
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.untyped) }
328
+ sig { params(dep_name: String).returns(T.nilable(String)) }
326
329
  def version_from_lockfile(dep_name)
327
- return unless parsed_lockfile
330
+ return unless poetry_lock
328
331
 
329
- parsed_lockfile.fetch("package", [])
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 { params(req: T.untyped).returns(T::Array[Dependabot::Python::Requirement]) }
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["version"]
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 { params(source_value: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
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
- poetry_sources = parsed_pyproject.dig("tool", "poetry", "source") || []
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["url"],
376
+ url: source_def.url,
371
377
  name: source_name
372
378
  }
373
379
  end
374
380
 
375
- sig { returns(T::Hash[String, T.untyped]) }
376
- def parsed_pyproject
377
- @parsed_pyproject ||= T.let(TomlRB.parse(T.must(pyproject).content), T.nilable(T::Hash[String, T.untyped]))
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(T::Hash[String, T.untyped]) }
383
- def parsed_poetry_lock
384
- @parsed_poetry_lock ||= T.let(
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.untyped) }
399
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
401
400
  def lockfile
402
401
  poetry_lock
403
402
  end
404
403
 
405
- sig { returns(T.untyped) }
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
- 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]
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 MAIN_PYPI_INDEXES.include?(index_url)
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
- metadata = fetch_from_json_registry(index_url)
91
+ return fetch_from_simple_registry(index_url) unless main_pypi_index?(index_url)
94
92
 
95
- return metadata if metadata&.any?
93
+ begin
94
+ metadata = fetch_from_json_registry(index_url)
95
+
96
+ return metadata if metadata&.any?
96
97
 
97
- Dependabot.logger.warn("No valid versions found via JSON API. Falling back to HTML.")
98
- fetch_from_html_registry(index_url)
99
- rescue StandardError => e
100
- Dependabot.logger.warn("Unexpected error in JSON fetch: #{e.message}. Falling back to HTML.")
101
- fetch_from_html_registry(index_url)
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
- if index_response.status == 401 || index_response.status == 403
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
- release_data = release_data_array.last
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" => APPLICATION_TEXT }
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.sub(%r{/+$}, "") + "/"
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.gsub(%r{/*$}, "") + "/" }
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).gsub(%r{/*$}, "") + "/"
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").gsub(%r{/*$}, "") + "/"
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::Experiments.enabled?(:enable_exclude_paths_subdirectory_manifest_files) &&
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.393.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.393.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.393.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: '0.22'
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: '0.22'
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.393.0
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