dependabot-python 0.397.0 → 0.399.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: 34c523af279e445c79d075ebf5b490c037d934602da7f626250e10e421be7dd2
4
- data.tar.gz: 55c074f5276a81f6fd3f6194fd8241106bcc1d1bea16bc0b2eba7a3890b97461
3
+ metadata.gz: e243e407de697945437e9818b4a451ab39ecb274ea6281b9ddcd2270d1bd16e7
4
+ data.tar.gz: 2ff42e6c9c205775d467942431d3c8aa51e2c4db9557b7bfbea733e2b201cfae
5
5
  SHA512:
6
- metadata.gz: 24f9cd2e78f256a02a6d4808a899476c441b32142050d845e5f238e37b01a45fefe4f874d661da5e535e9fdebd1006cd0891ef5f9b67199203fcd849fc5fe54a
7
- data.tar.gz: 39d66f3dc7fe82c0f0a89ccf7133cf400602cbd284cb2412fef799d80637ea2b83a6cc155bd94cd7837918463ef7dd0e556e809cc083009aaeb449e8aef1190a
6
+ metadata.gz: '0297caeec2ee5d809878d232089fabb5015966cb703bf59f3fcbdd295ca87908be3bffbe447d9c32b79b2945796f4c7d2a985574643496fe45e69116fb77a7fc'
7
+ data.tar.gz: 9f2c74c8fca0e487014943a033c030c67907c0f9135ae802bf4bd8d38dab8c876f18e0f05f98f7cae033906555ce0981c54b48671dd5d4d345e9b892576dda96
@@ -1,10 +1,10 @@
1
1
  pip==26.2.1
2
2
  pip-tools==7.6.1
3
- flake8==7.3.0
3
+ flake8==7.4.1
4
4
  hashin==1.0.5
5
5
  pipenv==2024.4.1
6
6
  plette==2.2.1
7
- poetry==2.4.1
7
+ poetry==2.5.1
8
8
  pytest==9.1.1
9
9
  # TODO: Replace 3p package `tomli` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10.
10
10
  tomli==2.4.1
@@ -0,0 +1,105 @@
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 PipfileDocument
15
+ extend T::Sig
16
+
17
+ class Entry
18
+ extend T::Sig
19
+
20
+ sig { params(name: String, value: Object, context: String).void }
21
+ def initialize(name:, value:, context:)
22
+ @name = name
23
+ @context = context
24
+ @value = T.let(
25
+ value.is_a?(String) ? value : PyprojectValueParser.object_hash(value, context),
26
+ T.any(String, PyprojectValueParser::ObjectHash)
27
+ )
28
+ end
29
+
30
+ sig { returns(String) }
31
+ attr_reader :name
32
+
33
+ sig { returns(T::Boolean) }
34
+ def specifies_version?
35
+ value = @value
36
+ return true if value.is_a?(String)
37
+
38
+ version = value["version"]
39
+ return false if version.nil? || version == false
40
+ return true if version.is_a?(String) || version == true
41
+
42
+ raise TypeError, "#{@context} version must be a string or boolean"
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def git_or_path?
47
+ value = @value
48
+ value.is_a?(Hash) && (value.key?("git") || value.key?("path"))
49
+ end
50
+
51
+ sig { returns(String) }
52
+ def requirement
53
+ value = @value
54
+ return value.empty? ? "*" : value if value.is_a?(String)
55
+
56
+ PyprojectValueParser.string(value["version"], "#{@context} version")
57
+ end
58
+
59
+ sig { returns(T.nilable(String)) }
60
+ def lookup_version
61
+ value = @value
62
+ return value.strip if value.is_a?(String)
63
+
64
+ PyprojectValueParser.optional_string(value["version"], "#{@context} version")
65
+ end
66
+
67
+ sig { returns(T.nilable(String)) }
68
+ def lockfile_version
69
+ value = @value
70
+ version = value.is_a?(String) ? value : value["version"]
71
+ return unless version
72
+ return if git_or_path?
73
+
74
+ PyprojectValueParser.string(version, "#{@context} version")
75
+ end
76
+ end
77
+
78
+ sig { params(file: Dependabot::DependencyFile).returns(PipfileDocument) }
79
+ def self.from_file(file)
80
+ new(data: T.cast(TomlRB.parse(T.must(file.content)), Object), context: file.path)
81
+ rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
82
+ raise Dependabot::DependencyFileNotParseable, file.path
83
+ end
84
+
85
+ sig { params(data: Object, context: String).void }
86
+ def initialize(data:, context:)
87
+ @data = T.let(PyprojectValueParser.object_hash(data, context), PyprojectValueParser::ObjectHash)
88
+ @context = context
89
+ end
90
+
91
+ sig { params(group: String).returns(T::Array[Entry]) }
92
+ def entries(group)
93
+ value = @data[group]
94
+ return [] unless value
95
+ return [] if value.is_a?(Array) && value.empty?
96
+
97
+ context = "#{@context} #{group}"
98
+ PyprojectValueParser.object_hash(value, context).map do |name, requirement|
99
+ Entry.new(name: name, value: requirement, context: "#{context} entry")
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,17 +1,14 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
- require "toml-rb"
5
-
6
4
  require "dependabot/dependency"
7
5
  require "dependabot/file_parsers/base/dependency_set"
8
6
  require "dependabot/python/file_parser"
9
- require "dependabot/errors"
10
7
  require "dependabot/python/name_normaliser"
11
8
 
12
9
  module Dependabot
13
10
  module Python
14
- class FileParser
11
+ class FileParser < Dependabot::FileParsers::Base
15
12
  class PipfileFilesParser
16
13
  extend T::Sig
17
14
 
@@ -54,30 +51,24 @@ module Dependabot
54
51
  dependencies = Dependabot::FileParsers::Base::DependencySet.new
55
52
 
56
53
  DEPENDENCY_GROUP_KEYS.each do |keys|
57
- next unless parsed_pipfile[T.must(keys[:pipfile])]
58
-
59
- parsed_pipfile[T.must(keys[:pipfile])].map do |dep_name, req|
60
- group = keys[:lockfile]
61
- next unless specifies_version?(req)
62
- next if git_or_path_requirement?(req)
63
- next if pipfile_lock && !dependency_version(dep_name, req, T.must(group))
64
-
65
- # Empty requirements are not allowed in Dependabot::Dependency and
66
- # equivalent to "*" (latest available version)
67
- req = "*" if req == ""
54
+ pipfile_document.entries(keys.fetch(:pipfile)).each do |entry|
55
+ group = keys.fetch(:lockfile)
56
+ next unless entry.specifies_version?
57
+ next if entry.git_or_path?
58
+ next if pipfile_lock && !dependency_version(entry, group)
68
59
 
69
60
  dependencies <<
70
61
  Dependency.new(
71
- name: normalised_name(dep_name),
72
- version: dependency_version(dep_name, req, T.must(group)),
62
+ name: normalised_name(entry.name),
63
+ version: dependency_version(entry, group),
73
64
  requirements: [{
74
- requirement: req.is_a?(String) ? req : req["version"],
65
+ requirement: entry.requirement,
75
66
  file: T.must(pipfile).name,
76
67
  source: nil,
77
68
  groups: [group]
78
69
  }],
79
70
  package_manager: "pip",
80
- metadata: { original_name: dep_name }
71
+ metadata: { original_name: entry.name }
81
72
  )
82
73
  end
83
74
  end
@@ -94,21 +85,14 @@ module Dependabot
94
85
  return dependencies unless pipfile_lock
95
86
 
96
87
  DEPENDENCY_GROUP_KEYS.map { |h| h.fetch(:lockfile) }.each do |key|
97
- section_data = parsed_pipfile_lock_section(key)
98
- next unless section_data
99
-
100
- section_data.each do |dep_name, details|
101
- version = case details
102
- when String then details
103
- when Hash then details["version"]
104
- end
88
+ pipfile_lock_document.entries(key).each do |entry|
89
+ version = entry.lockfile_version
105
90
  next unless version
106
- next if git_or_path_requirement?(details)
107
91
 
108
92
  dependencies <<
109
93
  Dependency.new(
110
- name: dep_name,
111
- version: version&.gsub(/^===?/, ""),
94
+ name: entry.name,
95
+ version: version.gsub(/^===?/, ""),
112
96
  requirements: [],
113
97
  package_manager: "pip",
114
98
  subdependency_metadata: [{ production: key != "develop" }]
@@ -119,78 +103,34 @@ module Dependabot
119
103
  dependencies
120
104
  end
121
105
 
122
- sig do
123
- params(
124
- dep_name: String,
125
- requirement: T.any(String, T::Hash[String, T.untyped]),
126
- group: String
127
- ).returns(T.nilable(String))
128
- end
129
- def dependency_version(dep_name, requirement, group)
130
- req = version_from_hash_or_string(requirement)
106
+ sig { params(entry: PipfileDocument::Entry, group: String).returns(T.nilable(String)) }
107
+ def dependency_version(entry, group)
108
+ req = entry.lookup_version
131
109
 
132
110
  if pipfile_lock
133
- details = parsed_pipfile_lock_section(group)&.[](normalised_name(dep_name))
134
-
135
- version = version_from_hash_or_string(details)
111
+ version = pipfile_lock_document.version_for(group, normalised_name(entry.name))
136
112
  version&.gsub(/^===?/, "")
137
113
  elsif T.must(req).start_with?("==") && !T.must(req).include?("*")
138
114
  T.must(req).strip.gsub(/^===?/, "")
139
115
  end
140
116
  end
141
117
 
142
- sig do
143
- params(obj: T.nilable(T.any(String, T::Array[String], T::Hash[String, T.untyped]))).returns(T.nilable(String))
144
- end
145
- def version_from_hash_or_string(obj)
146
- case obj
147
- when String then obj.strip
148
- when Hash then obj["version"]
149
- end
150
- end
151
-
152
- sig { params(req: T.any(String, T::Hash[String, T.untyped])).returns(T.nilable(T.any(T::Boolean, String))) }
153
- def specifies_version?(req)
154
- return true if req.is_a?(String)
155
-
156
- req["version"]
157
- end
158
-
159
- sig { params(req: T.any(String, T::Hash[String, T.untyped])).returns(T::Boolean) }
160
- def git_or_path_requirement?(req)
161
- return false unless req.is_a?(Hash)
162
-
163
- %w(git path).any? { |k| req.key?(k) }
164
- end
165
-
166
118
  sig { params(name: String, extras: T::Array[String]).returns(String) }
167
119
  def normalised_name(name, extras = [])
168
120
  NameNormaliser.normalise_including_extras(name, extras)
169
121
  end
170
122
 
171
- sig { returns(T::Hash[String, T.untyped]) }
172
- def parsed_pipfile
173
- @parsed_pipfile ||= T.let(TomlRB.parse(T.must(pipfile).content), T.nilable(T::Hash[String, T.untyped]))
174
- rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
175
- raise Dependabot::DependencyFileNotParseable, T.must(pipfile).path
123
+ sig { returns(PipfileDocument) }
124
+ def pipfile_document
125
+ @pipfile_document ||= T.let(PipfileDocument.from_file(T.must(pipfile)), T.nilable(PipfileDocument))
176
126
  end
177
127
 
178
- sig { returns(T::Hash[String, T.untyped]) }
179
- def parsed_pipfile_lock
180
- @parsed_pipfile_lock ||= T.let(
181
- JSON.parse(T.must(T.must(pipfile_lock).content)),
182
- T.nilable(T::Hash[String, T.untyped])
128
+ sig { returns(PipfileLockDocument) }
129
+ def pipfile_lock_document
130
+ @pipfile_lock_document ||= T.let(
131
+ PipfileLockDocument.from_file(T.must(pipfile_lock)),
132
+ T.nilable(PipfileLockDocument)
183
133
  )
184
- rescue JSON::ParserError
185
- raise Dependabot::DependencyFileNotParseable, T.must(pipfile_lock).path
186
- end
187
-
188
- sig { params(section: String).returns(T.nilable(T::Hash[String, T.untyped])) }
189
- def parsed_pipfile_lock_section(section)
190
- section_data = parsed_pipfile_lock[section]
191
- return section_data if section_data.is_a?(Hash)
192
-
193
- nil
194
134
  end
195
135
 
196
136
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
@@ -0,0 +1,67 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "sorbet-runtime"
6
+
7
+ require "dependabot/dependency_file"
8
+ require "dependabot/errors"
9
+ require "dependabot/python/file_parser"
10
+ require "dependabot/python/file_parser/pipfile_document"
11
+
12
+ module Dependabot
13
+ module Python
14
+ class FileParser < Dependabot::FileParsers::Base
15
+ class PipfileLockDocument
16
+ extend T::Sig
17
+
18
+ sig { params(file: Dependabot::DependencyFile).returns(PipfileLockDocument) }
19
+ def self.from_file(file)
20
+ new(data: T.cast(JSON.parse(T.must(file.content)), Object), context: file.path)
21
+ rescue JSON::ParserError
22
+ raise Dependabot::DependencyFileNotParseable, file.path
23
+ end
24
+
25
+ sig { params(data: Object, context: String).void }
26
+ def initialize(data:, context:)
27
+ @data = T.let(PyprojectValueParser.object_hash(data, context), PyprojectValueParser::ObjectHash)
28
+ @context = context
29
+ end
30
+
31
+ sig { params(group: String).returns(T::Array[PipfileDocument::Entry]) }
32
+ def entries(group)
33
+ entries = section(group)
34
+ return [] unless entries
35
+
36
+ entries.filter_map do |name, value|
37
+ next unless value.is_a?(String) || value.is_a?(Hash)
38
+
39
+ PipfileDocument::Entry.new(name: name, value: value, context: "#{@context} #{group} entry")
40
+ end
41
+ end
42
+
43
+ sig { params(group: String, name: String).returns(T.nilable(String)) }
44
+ def version_for(group, name)
45
+ value = section(group)&.[](name)
46
+ return if value.nil? || value.is_a?(Array)
47
+
48
+ unless value.is_a?(String) || value.is_a?(Hash)
49
+ raise TypeError, "#{@context} #{group} entry must be a string, object, or array"
50
+ end
51
+
52
+ PipfileDocument::Entry.new(name: name, value: value, context: "#{@context} #{group} entry").lookup_version
53
+ end
54
+
55
+ private
56
+
57
+ sig { params(group: String).returns(T.nilable(PyprojectValueParser::ObjectHash)) }
58
+ def section(group)
59
+ value = @data[group]
60
+ return unless value.is_a?(Hash)
61
+
62
+ PyprojectValueParser.object_hash(value, "#{@context} #{group}")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -202,6 +202,34 @@ module Dependabot
202
202
  PyprojectValueParser.string_array(value, "project.dynamic")
203
203
  end
204
204
 
205
+ # Pip resolution ignores malformed containers and non-string entries in these fields.
206
+ sig { returns(T::Array[String]) }
207
+ def pip_dependencies
208
+ project = @data["project"]
209
+ return [] unless project.is_a?(Hash)
210
+
211
+ dependencies = T.cast(project["dependencies"], Object)
212
+ return [] unless dependencies.is_a?(Array)
213
+
214
+ dependencies.grep(String)
215
+ end
216
+
217
+ sig { returns(T::Array[String]) }
218
+ def pip_constraint_paths
219
+ tool = @data["tool"]
220
+ return [] unless tool.is_a?(Hash)
221
+
222
+ pip = T.cast(tool["pip"], Object)
223
+ return [] unless pip.is_a?(Hash)
224
+
225
+ constraints = T.cast(pip["constraints"], Object)
226
+ case constraints
227
+ when String then [constraints]
228
+ when Array then constraints.grep(String)
229
+ else []
230
+ end
231
+ end
232
+
205
233
  sig { params(name: String).returns(T::Boolean) }
206
234
  def optional_dependency_group?(name)
207
235
  project = section(@data, "project", "project")
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency"
@@ -12,10 +12,21 @@ require "sorbet-runtime"
12
12
 
13
13
  module Dependabot
14
14
  module Python
15
- class FileParser
15
+ class FileParser < Dependabot::FileParsers::Base
16
16
  class SetupFileParser
17
17
  extend T::Sig
18
18
 
19
+ class SetupDependency < T::ImmutableStruct
20
+ const :name, String
21
+ const :version, T.nilable(String)
22
+ const :markers, T.nilable(String)
23
+ const :file, String
24
+ const :requirement, T.nilable(String)
25
+ const :requirement_type, String
26
+ const :extras, T::Array[String]
27
+ end
28
+ private_constant :SetupDependency
29
+
19
30
  INSTALL_REQUIRES_REGEX = /install_requires\s*=\s*\[/m
20
31
  SETUP_REQUIRES_REGEX = /setup_requires\s*=\s*\[/m
21
32
  TESTS_REQUIRE_REGEX = /tests_require\s*=\s*\[/m
@@ -35,24 +46,24 @@ module Dependabot
35
46
  parsed_setup_file.each do |dep|
36
47
  # If a requirement has a `<` or `<=` marker then updating it is
37
48
  # probably blocked. Ignore it.
38
- next if dep["markers"].include?("<")
49
+ next if dep.markers&.include?("<")
39
50
 
40
51
  # If the requirement is our inserted version, ignore it
41
52
  # (we wouldn't be able to update it)
42
- next if dep["version"] == "0.0.1+dependabot"
53
+ next if dep.version == "0.0.1+dependabot"
43
54
 
44
55
  dependencies <<
45
56
  Dependency.new(
46
- name: normalise(dep["name"]),
47
- version: dep["version"]&.include?("*") ? nil : dep["version"],
57
+ name: normalise(dep.name),
58
+ version: dep.version&.include?("*") ? nil : dep.version,
48
59
  requirements: [{
49
- requirement: dep["requirement"],
50
- file: Pathname.new(dep["file"]).cleanpath.to_path,
60
+ requirement: dep.requirement,
61
+ file: Pathname.new(dep.file).cleanpath.to_path,
51
62
  source: nil,
52
- groups: [dep["requirement_type"]]
63
+ groups: [dep.requirement_type]
53
64
  }],
54
65
  package_manager: "pip",
55
- metadata: extras_metadata(dep["extras"])
66
+ metadata: extras_metadata(dep.extras)
56
67
  )
57
68
  end
58
69
  dependencies
@@ -63,59 +74,26 @@ module Dependabot
63
74
  sig { returns(T::Array[Dependabot::DependencyFile]) }
64
75
  attr_reader :dependency_files
65
76
 
66
- sig { returns(T.untyped) }
77
+ sig { returns(T::Array[SetupDependency]) }
67
78
  def parsed_setup_file
68
79
  SharedHelpers.in_a_temporary_directory do
69
80
  write_temporary_dependency_files
70
-
71
- requirements = T.cast(
72
- SharedHelpers.run_helper_subprocess(
73
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
74
- function: "parse_setup",
75
- args: [Dir.pwd]
76
- ),
77
- T.nilable(
78
- T.any(
79
- T::Hash[String, T.untyped],
80
- String,
81
- T::Array[T::Hash[String, T.untyped]]
82
- )
83
- )
84
- )
85
-
86
- check_requirements(requirements)
87
- requirements
81
+ run_setup_helper
88
82
  end
89
83
  rescue SharedHelpers::HelperSubprocessFailed => e
90
84
  raise Dependabot::DependencyFileNotEvaluatable, e.message if e.message.start_with?("InstallationError")
91
85
 
92
- return [] unless setup_file
86
+ file = setup_file
87
+ return [] unless file
93
88
 
94
- parsed_sanitized_setup_file
89
+ parsed_sanitized_setup_file(file)
95
90
  end
96
91
 
97
- sig { returns(T.nilable(T.any(T::Hash[String, T.untyped], String, T::Array[T::Hash[String, T.untyped]]))) }
98
- def parsed_sanitized_setup_file
92
+ sig { params(file: Dependabot::DependencyFile).returns(T::Array[SetupDependency]) }
93
+ def parsed_sanitized_setup_file(file)
99
94
  SharedHelpers.in_a_temporary_directory do
100
- write_sanitized_setup_file
101
-
102
- requirements = T.cast(
103
- SharedHelpers.run_helper_subprocess(
104
- command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
105
- function: "parse_setup",
106
- args: [Dir.pwd]
107
- ),
108
- T.nilable(
109
- T.any(
110
- T::Hash[String, T.untyped],
111
- String,
112
- T::Array[T::Hash[String, T.untyped]]
113
- )
114
- )
115
- )
116
-
117
- check_requirements(requirements)
118
- requirements
95
+ write_sanitized_setup_file(T.must(file.content))
96
+ run_setup_helper
119
97
  end
120
98
  rescue SharedHelpers::HelperSubprocessFailed
121
99
  # Assume there are no dependencies in setup.py files that fail to
@@ -125,15 +103,36 @@ module Dependabot
125
103
  []
126
104
  end
127
105
 
128
- sig { params(requirements: T.untyped).returns(T.untyped) }
129
- def check_requirements(requirements)
130
- requirements&.each do |dep|
131
- next unless dep["requirement"]
106
+ sig { returns(T::Array[SetupDependency]) }
107
+ def run_setup_helper
108
+ result = SharedHelpers.run_helper_subprocess(
109
+ command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
110
+ function: "parse_setup",
111
+ args: [Dir.pwd]
112
+ )
113
+ parse_setup_result(result)
114
+ end
132
115
 
133
- Python::Requirement.new(dep["requirement"].split(","))
134
- rescue Gem::Requirement::BadRequirementError => e
135
- raise Dependabot::DependencyFileNotEvaluatable, e.message
116
+ sig { params(result: Object).returns(T::Array[SetupDependency]) }
117
+ def parse_setup_result(result)
118
+ PyprojectValueParser.array(result, "parse_setup result").each_with_index.map do |value, index|
119
+ context = "parse_setup result[#{index}]"
120
+ fields = PyprojectValueParser.object_hash(value, context)
121
+ requirement = PyprojectValueParser.optional_string(fields["requirement"], "#{context}.requirement")
122
+ Python::Requirement.new(requirement.split(",")) unless requirement.nil?
123
+
124
+ SetupDependency.new(
125
+ name: PyprojectValueParser.string(fields["name"], "#{context}.name"),
126
+ version: PyprojectValueParser.optional_string(fields["version"], "#{context}.version"),
127
+ markers: PyprojectValueParser.optional_string(fields["markers"], "#{context}.markers"),
128
+ file: PyprojectValueParser.string(fields["file"], "#{context}.file"),
129
+ requirement: requirement,
130
+ requirement_type: PyprojectValueParser.string(fields["requirement_type"], "#{context}.requirement_type"),
131
+ extras: PyprojectValueParser.string_array(fields["extras"], "#{context}.extras")
132
+ )
136
133
  end
134
+ rescue TypeError, Gem::Requirement::BadRequirementError => e
135
+ raise Dependabot::DependencyFileNotEvaluatable, e.message
137
136
  end
138
137
 
139
138
  sig { void }
@@ -152,12 +151,12 @@ module Dependabot
152
151
  # This sanitization is far from perfect (it will fail if any of the
153
152
  # entries are dynamic), but it is an alternative approach to the one
154
153
  # used in parser.py which sometimes succeeds when that has failed.
155
- sig { void }
156
- def write_sanitized_setup_file
157
- install_requires = get_regexed_req_array(INSTALL_REQUIRES_REGEX)
158
- setup_requires = get_regexed_req_array(SETUP_REQUIRES_REGEX)
159
- tests_require = get_regexed_req_array(TESTS_REQUIRE_REGEX)
160
- extras_require = get_regexed_req_dict(EXTRAS_REQUIRE_REGEX)
154
+ sig { params(content: String).void }
155
+ def write_sanitized_setup_file(content)
156
+ install_requires = get_regexed_req_array(content, INSTALL_REQUIRES_REGEX)
157
+ setup_requires = get_regexed_req_array(content, SETUP_REQUIRES_REGEX)
158
+ tests_require = get_regexed_req_array(content, TESTS_REQUIRE_REGEX)
159
+ extras_require = get_regexed_req_dict(content, EXTRAS_REQUIRE_REGEX)
161
160
 
162
161
  tmp = "from setuptools import setup\n\n" \
163
162
  "setup(name=\"sanitized-package\",version=\"0.0.1\","
@@ -171,16 +170,16 @@ module Dependabot
171
170
  File.write("setup.py", tmp)
172
171
  end
173
172
 
174
- sig { params(regex: Regexp).returns(T.nilable(String)) }
175
- def get_regexed_req_array(regex)
176
- return unless (mch = setup_file.content.match(regex))
173
+ sig { params(content: String, regex: Regexp).returns(T.nilable(String)) }
174
+ def get_regexed_req_array(content, regex)
175
+ return unless (mch = content.match(regex))
177
176
 
178
177
  "[#{mch.post_match[0..closing_bracket_index(mch.post_match, '[')]}"
179
178
  end
180
179
 
181
- sig { params(regex: Regexp).returns(T.nilable(String)) }
182
- def get_regexed_req_dict(regex)
183
- return unless (mch = setup_file.content.match(regex))
180
+ sig { params(content: String, regex: Regexp).returns(T.nilable(String)) }
181
+ def get_regexed_req_dict(content, regex)
182
+ return unless (mch = content.match(regex))
184
183
 
185
184
  "{#{mch.post_match[0..closing_bracket_index(mch.post_match, '{')]}"
186
185
  end
@@ -215,7 +214,7 @@ module Dependabot
215
214
  { extras: extras.join(",") }
216
215
  end
217
216
 
218
- sig { returns(T.untyped) }
217
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
219
218
  def setup_file
220
219
  dependency_files.find { |f| f.name == "setup.py" }
221
220
  end
@@ -24,6 +24,8 @@ module Dependabot
24
24
  require_relative "file_parser/pyproject_document"
25
25
  require_relative "file_parser/poetry_lock"
26
26
  require_relative "file_parser/pep_dependency"
27
+ require_relative "file_parser/pipfile_document"
28
+ require_relative "file_parser/pipfile_lock_document"
27
29
  require_relative "file_parser/pipfile_files_parser"
28
30
  require_relative "file_parser/pyproject_files_parser"
29
31
  require_relative "file_parser/setup_file_parser"
@@ -363,9 +363,19 @@ module Dependabot
363
363
  content = replace_header_with_original(updated_content, T.must(file.content))
364
364
  content = remove_new_warnings(content, T.must(file.content))
365
365
  content = update_hashes_if_required(content, T.must(file.content))
366
+ content = scrub_index_url_credentials(content)
366
367
  replace_absolute_file_paths(content, T.must(file.content))
367
368
  end
368
369
 
370
+ # Index URLs are passed to pip-compile with credentials embedded so the
371
+ # private index can be fetched, and pip-tools emits those URLs verbatim
372
+ # into the compiled file when index-url emission is active. Strip any
373
+ # userinfo so tokens are never written into the regenerated file.
374
+ sig { params(content: String).returns(String) }
375
+ def scrub_index_url_credentials(content)
376
+ content.gsub(%r{^(--(?:extra-)?index-url[= ]https?://)[^/\s]+@}, "\\1")
377
+ end
378
+
369
379
  sig { params(updated_content: String, original_content: String).returns(String) }
370
380
  def replace_header_with_original(updated_content, original_content)
371
381
  original_header_lines =
@@ -16,7 +16,7 @@ module Dependabot
16
16
  # e.g. ARG PY_3_13=3.13.x
17
17
  # Note: uv ecosystem aliases this class, so updates here apply to both ecosystems.
18
18
  PRE_INSTALLED_PYTHON_VERSIONS_RAW = %w(
19
- 3.14.5
19
+ 3.14.7
20
20
  3.13.13
21
21
  3.12.13
22
22
  3.11.15
@@ -13,6 +13,7 @@ require "dependabot/python/package/package_registry_finder"
13
13
  require "dependabot/python/update_checker"
14
14
  require "dependabot/python/update_checker/latest_version_finder"
15
15
  require "dependabot/python/file_parser/python_requirement_parser"
16
+ require "dependabot/python/file_parser/pyproject_document"
16
17
 
17
18
  module Dependabot
18
19
  module Python
@@ -62,7 +63,7 @@ module Dependabot
62
63
  @constraints_files = T.let(nil, T.nilable(T::Array[String]))
63
64
  @constraints_file_basenames = T.let(nil, T.nilable(T::Array[String]))
64
65
  @requirement_file_directories = T.let(nil, T.nilable(T::Array[String]))
65
- @pyproject_content_cache = T.let({}, T::Hash[String, T::Hash[String, Object]])
66
+ @pyproject_document_cache = T.let({}, T::Hash[String, FileParser::PyprojectDocument])
66
67
  end
67
68
 
68
69
  sig { returns(T.nilable(Dependabot::Version)) }
@@ -201,19 +202,8 @@ module Dependabot
201
202
 
202
203
  sig { params(pyproject: Dependabot::DependencyFile).returns(T::Array[[String, String]]) }
203
204
  def pinned_pyproject_dependencies_for(pyproject)
204
- pyproject_content = pyproject_content_for(pyproject)
205
- project_obj = pyproject_content["project"]
206
- return [] unless project_obj.is_a?(Hash)
207
-
208
- project_hash = project_obj
209
- dependencies_obj = T.cast(project_hash["dependencies"], T.nilable(Object))
210
- return [] unless dependencies_obj.is_a?(Array)
211
-
212
- dependencies_obj.filter_map do |entry|
213
- entry_obj = T.cast(entry, T.nilable(Object))
214
- next unless entry_obj.is_a?(String)
215
-
216
- requirement_string, marker = split_requirement_and_marker(entry_obj)
205
+ pyproject_document_for(pyproject).pip_dependencies.filter_map do |entry|
206
+ requirement_string, marker = split_requirement_and_marker(entry)
217
207
  next unless marker_satisfied_for_python?(marker)
218
208
  next if requirement_string.nil? || requirement_string.empty?
219
209
 
@@ -432,23 +422,8 @@ module Dependabot
432
422
 
433
423
  sig { params(pyproject: Dependabot::DependencyFile).returns(T::Array[String]) }
434
424
  def constraints_for_pyproject(pyproject)
435
- pyproject_content = pyproject_content_for(pyproject)
436
- tool_obj = pyproject_content["tool"]
437
- return [] unless tool_obj.is_a?(Hash)
438
-
439
- pip_obj = T.cast(tool_obj["pip"], T.nilable(Object))
440
- return [] unless pip_obj.is_a?(Hash)
441
-
442
- constraints_obj = T.cast(pip_obj["constraints"], T.nilable(Object))
443
- case constraints_obj
444
- when String
445
- [resolve_constraint_path(path: constraints_obj, declaring_file: pyproject)]
446
- when Array
447
- constraints_obj.grep(String).map do |path|
448
- resolve_constraint_path(path: path, declaring_file: pyproject)
449
- end
450
- else
451
- []
425
+ pyproject_document_for(pyproject).pip_constraint_paths.map do |path|
426
+ resolve_constraint_path(path: path, declaring_file: pyproject)
452
427
  end
453
428
  end
454
429
 
@@ -556,23 +531,22 @@ module Dependabot
556
531
  marker_evaluator.marker_satisfied?(marker: marker, python_version: python_version)
557
532
  end
558
533
 
559
- sig { params(pyproject: Dependabot::DependencyFile).returns(T::Hash[String, Object]) }
560
- def pyproject_content_for(pyproject)
534
+ sig { params(pyproject: Dependabot::DependencyFile).returns(FileParser::PyprojectDocument) }
535
+ def pyproject_document_for(pyproject)
561
536
  cache_key = pyproject.name
562
- return T.must(@pyproject_content_cache[cache_key]) if @pyproject_content_cache.key?(cache_key)
537
+ return T.must(@pyproject_document_cache[cache_key]) if @pyproject_document_cache.key?(cache_key)
563
538
 
564
- content =
565
- if pyproject.content
566
- T.let(TomlRB.parse(pyproject.content), T::Hash[String, Object])
539
+ content = pyproject.content
540
+ document =
541
+ if content
542
+ FileParser::PyprojectDocument.from_content(content)
567
543
  else
568
- T.let({}, T::Hash[String, Object])
544
+ FileParser::PyprojectDocument.new({})
569
545
  end
570
546
 
571
- @pyproject_content_cache[cache_key] = content
572
- content
547
+ @pyproject_document_cache[cache_key] = document
573
548
  rescue TomlRB::ParseError, TomlRB::ValueOverwriteError
574
- @pyproject_content_cache[pyproject.name] = {}
575
- T.must(@pyproject_content_cache[pyproject.name])
549
+ @pyproject_document_cache[pyproject.name] = FileParser::PyprojectDocument.new({})
576
550
  end
577
551
  end
578
552
  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.397.0
4
+ version: 0.399.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.397.0
18
+ version: 0.399.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.397.0
25
+ version: 0.399.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -281,7 +281,9 @@ files:
281
281
  - lib/dependabot/python/file_fetcher.rb
282
282
  - lib/dependabot/python/file_parser.rb
283
283
  - lib/dependabot/python/file_parser/pep_dependency.rb
284
+ - lib/dependabot/python/file_parser/pipfile_document.rb
284
285
  - lib/dependabot/python/file_parser/pipfile_files_parser.rb
286
+ - lib/dependabot/python/file_parser/pipfile_lock_document.rb
285
287
  - lib/dependabot/python/file_parser/poetry_lock.rb
286
288
  - lib/dependabot/python/file_parser/pyproject_document.rb
287
289
  - lib/dependabot/python/file_parser/pyproject_files_parser.rb
@@ -327,7 +329,7 @@ licenses:
327
329
  - MIT
328
330
  metadata:
329
331
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
330
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.397.0
332
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.399.0
331
333
  rdoc_options: []
332
334
  require_paths:
333
335
  - lib