dependabot-python 0.357.0 → 0.359.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: 4d9c1af8569ef4090792c2ebee4bd6dd19e58e47c0d6bc1918804c26adeb4dfc
4
- data.tar.gz: 5bb14421ad8c437294aba6d0d5a0bc0bca2c6dd61e8f7d998773c13f73750f5b
3
+ metadata.gz: 9b809922cec5a45858a2db5ccbc1a44616bd29e338b208ef2ce62c40cc92c5cc
4
+ data.tar.gz: 690573daf64882e4308e2b67962d218b050d6e35f5f2de463037c9d3bb4373f5
5
5
  SHA512:
6
- metadata.gz: 9d91cabe7351ee1d2dcc0fcbe842d9cae3141b13cfbc313d683e4f2723102b9ecadb1fbf5545dc4a3de615a40eb6fcdd9669b84a2c1350df5420cd92486c38ec
7
- data.tar.gz: b4be22afa1102a99dd8e3dc4a8935d28fd839d6917e029a1db19400dfd380b0f21c3c92e97b5aedf06c9b7eb5e6c46a1a90d0550cf37c1c6b80b158b863d8d53
6
+ metadata.gz: 85cbac2d3e04004f10fb8829b512c22962339aca71b3e2fe00b5b22a0af1a745cc6a0810d038f5bf084dcebd749959fb73bcf08408de67a5683a61d7eb47e121
7
+ data.tar.gz: f2336e9490af88f4a08ed69c4116b085d161e2e0e8ef5ea5e2e494fb1308ea5b62239761a675c4f214cd79b943a0414d7e2133a6a5e5573b4aac1ef433723a8a
@@ -19,7 +19,8 @@ module Dependabot
19
19
  POETRY_DEPENDENCY_TYPES = %w(dependencies dev-dependencies).freeze
20
20
 
21
21
  # https://python-poetry.org/docs/dependency-specification/
22
- UNSUPPORTED_DEPENDENCY_TYPES = %w(git path url).freeze
22
+ # Git dependencies with tags are now supported for version tracking
23
+ UNSUPPORTED_DEPENDENCY_TYPES = %w(path url).freeze
23
24
 
24
25
  sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
25
26
  def initialize(dependency_files:)
@@ -148,13 +149,29 @@ module Dependabot
148
149
 
149
150
  # @param req can be an Array, Hash or String that represents the constraints for a dependency
150
151
  sig { params(req: T.untyped, type: String).returns(T::Array[T::Hash[Symbol, T.nilable(String)]]) }
152
+ # rubocop:disable Metrics/PerceivedComplexity
151
153
  def parse_requirements_from(req, type)
152
154
  [req].flatten.compact.filter_map do |requirement|
155
+ # Skip unsupported dependency types (path, url), but allow git
153
156
  next if requirement.is_a?(Hash) && UNSUPPORTED_DEPENDENCY_TYPES.intersect?(requirement.keys)
157
+ # Skip git dependencies without tags (e.g., with branch, rev)
158
+ next if requirement.is_a?(Hash) && requirement["git"] && !requirement["tag"]
154
159
 
155
- check_requirements(requirement)
156
-
157
- if requirement.is_a?(String)
160
+ # Handle git dependencies with tags
161
+ if requirement.is_a?(Hash) && requirement["git"] && requirement["tag"]
162
+ {
163
+ requirement: nil,
164
+ file: T.must(pyproject).name,
165
+ source: {
166
+ type: "git",
167
+ url: requirement["git"],
168
+ ref: requirement["tag"],
169
+ branch: nil
170
+ },
171
+ groups: [type]
172
+ }
173
+ elsif requirement.is_a?(String)
174
+ check_requirements(requirement)
158
175
  {
159
176
  requirement: requirement,
160
177
  file: T.must(pyproject).name,
@@ -162,15 +179,21 @@ module Dependabot
162
179
  groups: [type]
163
180
  }
164
181
  else
182
+ check_requirements(requirement)
183
+ # String sources are registry name references (e.g., "custom") that reference
184
+ # [[tool.poetry.source]] definitions. Resolve them to proper hashes.
185
+ source_value = requirement.fetch("source", nil)
186
+ source = resolve_source(source_value)
165
187
  {
166
188
  requirement: requirement["version"],
167
189
  file: T.must(pyproject).name,
168
- source: requirement.fetch("source", nil),
190
+ source: source,
169
191
  groups: [type]
170
192
  }
171
193
  end
172
194
  end
173
195
  end
196
+ # rubocop:enable Metrics/PerceivedComplexity
174
197
 
175
198
  sig { returns(T.nilable(T::Boolean)) }
176
199
  def using_poetry?
@@ -280,6 +303,34 @@ module Dependabot
280
303
  NameNormaliser.normalise(name)
281
304
  end
282
305
 
306
+ sig { params(source_value: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
307
+ def resolve_source(source_value)
308
+ # Return nil if no source specified
309
+ return nil if source_value.nil?
310
+
311
+ # If already a hash, return as-is (handles git sources)
312
+ return source_value if source_value.is_a?(Hash)
313
+
314
+ # String sources are references to [[tool.poetry.source]] definitions
315
+ # Look up the source definition and create a hash
316
+ return nil unless source_value.is_a?(String)
317
+
318
+ source_name = source_value
319
+ poetry_sources = parsed_pyproject.dig("tool", "poetry", "source") || []
320
+ source_def = poetry_sources.find { |s| s["name"] == source_name }
321
+
322
+ # If source definition not found, return nil
323
+ return nil unless source_def
324
+
325
+ # Create a hash with type and url from the source definition
326
+ # Use "registry" as the type since these are package index sources
327
+ {
328
+ type: "registry",
329
+ url: source_def["url"],
330
+ name: source_name
331
+ }
332
+ end
333
+
283
334
  sig { returns(T.untyped) }
284
335
  def parsed_pyproject
285
336
  @parsed_pyproject ||= T.let(TomlRB.parse(T.must(pyproject).content), T.untyped)
@@ -101,16 +101,12 @@ module Dependabot
101
101
 
102
102
  sig { returns(Ecosystem::VersionManager) }
103
103
  def package_manager
104
- if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
105
- Dependabot.logger.info("Detected package manager : #{detected_package_manager.name}")
106
- end
107
-
108
104
  @package_manager ||= T.let(detected_package_manager, T.nilable(Dependabot::Ecosystem::VersionManager))
109
105
  end
110
106
 
111
107
  sig { returns(Ecosystem::VersionManager) }
112
108
  def detected_package_manager
113
- setup_python_environment if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
109
+ setup_python_environment
114
110
 
115
111
  return PipenvPackageManager.new(T.must(detect_pipenv_version)) if detect_pipenv_version
116
112
 
@@ -225,11 +221,6 @@ module Dependabot
225
221
 
226
222
  sig { returns(String) }
227
223
  def python_raw_version
228
- if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
229
- Dependabot.logger.info("Detected python version: #{language_version_manager.python_version}")
230
- Dependabot.logger.info("Detected python major minor version: #{language_version_manager.python_major_minor}")
231
- end
232
-
233
224
  language_version_manager.python_version
234
225
  end
235
226
 
@@ -115,6 +115,9 @@ module Dependabot
115
115
  ).returns(String)
116
116
  end
117
117
  def replace_dep(dep, content, new_r, old_r)
118
+ # Handle Git dependencies with tags
119
+ return update_git_tag(dep, content, new_r, old_r) if git_dependency?(new_r) && git_dependency?(old_r)
120
+
118
121
  new_req = new_r[:requirement]
119
122
  old_req = old_r[:requirement]
120
123
 
@@ -149,6 +152,38 @@ module Dependabot
149
152
  content
150
153
  end
151
154
 
155
+ sig { params(req: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
156
+ def git_dependency?(req)
157
+ req.dig(:source, :type) == "git"
158
+ end
159
+
160
+ sig do
161
+ params(
162
+ dep: Dependabot::Dependency,
163
+ content: String,
164
+ new_r: T::Hash[Symbol, T.untyped],
165
+ old_r: T::Hash[Symbol, T.untyped]
166
+ ).returns(String)
167
+ end
168
+ def update_git_tag(dep, content, new_r, old_r)
169
+ old_tag = old_r.dig(:source, :ref)
170
+ new_tag = new_r.dig(:source, :ref)
171
+
172
+ return content if old_tag == new_tag
173
+
174
+ # Match git dependency declaration with tag
175
+ # Example: fastapi = { git = "...", extras = ["all"], tag = "0.110.0" }
176
+ git_dep_regex = /
177
+ ^(\s*)#{Regexp.escape(dep.name)}(\s*=\s*\{[^}]*tag\s*=\s*)
178
+ ["']#{Regexp.escape(old_tag)}["']([^}]*\})
179
+ /mx
180
+
181
+ content.gsub(git_dep_regex) do
182
+ match_data = T.must(Regexp.last_match)
183
+ "#{match_data[1]}#{dep.name}#{match_data[2]}\"#{new_tag}\"#{match_data[3]}"
184
+ end
185
+ end
186
+
152
187
  sig { returns(String) }
153
188
  def updated_lockfile_content
154
189
  @updated_lockfile_content ||=
@@ -199,6 +234,9 @@ module Dependabot
199
234
 
200
235
  if poetry_object
201
236
  dependencies.each do |dep|
237
+ # Skip Git dependencies - they use tags/refs, not versions
238
+ next if git_dependency_being_updated?(dep)
239
+
202
240
  if dep.requirements.find { |r| r[:file] == pyproject&.name }
203
241
  lock_declaration_to_new_version!(poetry_object, dep)
204
242
  else
@@ -240,6 +278,11 @@ module Dependabot
240
278
  poetry_object[subdep_type][dep.name] = dep.version
241
279
  end
242
280
 
281
+ sig { params(dep: Dependabot::Dependency).returns(T::Boolean) }
282
+ def git_dependency_being_updated?(dep)
283
+ dep.requirements.any? { |r| r.dig(:source, :type) == "git" }
284
+ end
285
+
243
286
  sig { params(pyproject_content: String).returns(String) }
244
287
  def sanitize(pyproject_content)
245
288
  PyprojectPreparer
@@ -1,4 +1,4 @@
1
- # typed: strong
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "cgi"
@@ -7,6 +7,7 @@ require "nokogiri"
7
7
  require "sorbet-runtime"
8
8
 
9
9
  require "dependabot/dependency"
10
+ require "dependabot/git_commit_checker"
10
11
  require "dependabot/python/update_checker"
11
12
  require "dependabot/update_checkers/version_filters"
12
13
  require "dependabot/registry_client"
@@ -22,6 +23,38 @@ module Dependabot
22
23
  class LatestVersionFinder < Dependabot::Package::PackageLatestVersionFinder
23
24
  extend T::Sig
24
25
 
26
+ sig do
27
+ params(git_commit_checker: Dependabot::GitCommitChecker)
28
+ .returns(T.nilable(T::Hash[Symbol, T.untyped]))
29
+ end
30
+ def latest_version_tag(git_commit_checker:)
31
+ return git_commit_checker.local_tag_for_latest_version unless cooldown_enabled?
32
+
33
+ allowed_version_tags = git_commit_checker.local_tags_for_allowed_versions
34
+ tags_in_cooldown = select_version_tags_in_cooldown_period(git_commit_checker)
35
+
36
+ return max_version_from_tags(allowed_version_tags) if tags_in_cooldown.empty?
37
+
38
+ filtered_tags = allowed_version_tags.reject do |tag|
39
+ tags_in_cooldown.include?(tag[:tag])
40
+ end
41
+
42
+ if filtered_tags.empty?
43
+ Dependabot.logger.info("All git tags filtered by cooldown for #{dependency.name}, returning nil")
44
+ return nil
45
+ end
46
+
47
+ filtered_count = allowed_version_tags.count - filtered_tags.count
48
+ if filtered_count.positive?
49
+ Dependabot.logger.info("Filtered #{filtered_count} git tags due to cooldown for #{dependency.name}")
50
+ end
51
+
52
+ max_version_from_tags(filtered_tags)
53
+ rescue StandardError => e
54
+ Dependabot.logger.error("Error fetching latest version tag: #{e.message}")
55
+ git_commit_checker.local_tag_for_latest_version
56
+ end
57
+
25
58
  sig do
26
59
  override.returns(T.nilable(Dependabot::Package::PackageDetails))
27
60
  end
@@ -35,7 +68,93 @@ module Dependabot
35
68
 
36
69
  sig { override.returns(T::Boolean) }
37
70
  def cooldown_enabled?
38
- true
71
+ return false if cooldown_options.nil?
72
+
73
+ cooldown = T.must(cooldown_options)
74
+ cooldown.default_days.to_i.positive? ||
75
+ cooldown.semver_major_days.to_i.positive? ||
76
+ cooldown.semver_minor_days.to_i.positive? ||
77
+ cooldown.semver_patch_days.to_i.positive?
78
+ end
79
+
80
+ private
81
+
82
+ sig { params(tags: T::Array[T::Hash[Symbol, T.untyped]]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
83
+ def max_version_from_tags(tags)
84
+ tags.max_by { |t| t[:version] }
85
+ end
86
+
87
+ sig { params(git_commit_checker: Dependabot::GitCommitChecker).returns(T::Array[String]) }
88
+ def select_version_tags_in_cooldown_period(git_commit_checker)
89
+ version_tags_in_cooldown_period = T.let([], T::Array[String])
90
+
91
+ git_commit_checker.refs_for_tag_with_detail.each do |git_tag_with_detail|
92
+ if check_if_version_in_cooldown_period?(git_tag_with_detail)
93
+ version_tags_in_cooldown_period << git_tag_with_detail.tag
94
+ end
95
+ end
96
+ version_tags_in_cooldown_period
97
+ rescue StandardError => e
98
+ Dependabot.logger.error("Error checking if version is in cooldown: #{e.message}")
99
+ []
100
+ end
101
+
102
+ sig { params(tag_with_detail: Dependabot::GitTagWithDetail).returns(T::Boolean) }
103
+ def check_if_version_in_cooldown_period?(tag_with_detail)
104
+ return false unless tag_with_detail.release_date
105
+
106
+ current_version = version_class.correct?(dependency.version) ? version_class.new(dependency.version) : nil
107
+ tag_version_str = tag_with_detail.tag.delete_prefix("v")
108
+ return false unless version_class.correct?(tag_version_str)
109
+
110
+ new_version = version_class.new(tag_version_str)
111
+ days = cooldown_days_for(current_version, new_version)
112
+
113
+ passed_seconds = Time.now.to_i - release_date_to_seconds(tag_with_detail.release_date)
114
+ passed_seconds < days * DAY_IN_SECONDS
115
+ end
116
+
117
+ sig do
118
+ params(
119
+ current_version: T.nilable(Dependabot::Version),
120
+ new_version: Dependabot::Version
121
+ ).returns(Integer)
122
+ end
123
+ def cooldown_days_for(current_version, new_version)
124
+ return 0 unless cooldown_enabled?
125
+
126
+ cooldown = T.must(cooldown_options)
127
+ return 0 unless cooldown.included?(dependency.name)
128
+ return cooldown.default_days if current_version.nil?
129
+
130
+ current_version_semver = current_version.semver_parts
131
+ new_version_semver = new_version.semver_parts
132
+
133
+ return cooldown.default_days if current_version_semver.nil? || new_version_semver.nil?
134
+
135
+ current_major, current_minor, current_patch = current_version_semver
136
+ new_major, new_minor, new_patch = new_version_semver
137
+
138
+ return cooldown.semver_major_days if new_major > current_major
139
+ return cooldown.semver_minor_days if new_minor > current_minor
140
+ return cooldown.semver_patch_days if new_patch > current_patch
141
+
142
+ cooldown.default_days
143
+ end
144
+
145
+ sig { returns(T.class_of(Dependabot::Version)) }
146
+ def version_class
147
+ dependency.version_class
148
+ end
149
+
150
+ sig { params(release_date: T.nilable(String)).returns(Integer) }
151
+ def release_date_to_seconds(release_date)
152
+ return 0 unless release_date
153
+
154
+ Time.parse(release_date).to_i
155
+ rescue ArgumentError => e
156
+ Dependabot.logger.error("Invalid release date format: #{release_date} and error: #{e.message}")
157
+ 0
39
158
  end
40
159
  end
41
160
  end
@@ -389,7 +389,7 @@ module Dependabot
389
389
  version = version.release if version.prerelease?
390
390
 
391
391
  lb_segments = version.segments
392
- lb_segments.pop while lb_segments.last.zero?
392
+ lb_segments.pop while lb_segments.last&.zero?
393
393
 
394
394
  lb_segments
395
395
  end
@@ -400,7 +400,7 @@ module Dependabot
400
400
  version = req.requirements.first.last.release
401
401
 
402
402
  if req_string.strip.start_with?("^")
403
- version.segments.index { |i| i != 0 }
403
+ version.segments.index { |i| i != 0 } || (version.segments.count - 1)
404
404
  elsif req_string.include?("*")
405
405
  version.segments.count - 1
406
406
  elsif req_string.strip.start_with?("~=", "==")
@@ -17,6 +17,7 @@ require "dependabot/update_checkers/base"
17
17
 
18
18
  module Dependabot
19
19
  module Python
20
+ # rubocop:disable Metrics/ClassLength
20
21
  class UpdateChecker < Dependabot::UpdateCheckers::Base
21
22
  extend T::Sig
22
23
 
@@ -35,6 +36,8 @@ module Dependabot
35
36
 
36
37
  sig { override.returns(T.nilable(Gem::Version)) }
37
38
  def latest_version
39
+ return latest_version_for_git_dependency if git_dependency?
40
+
38
41
  @latest_version ||= T.let(
39
42
  fetch_latest_version,
40
43
  T.nilable(Gem::Version)
@@ -43,6 +46,8 @@ module Dependabot
43
46
 
44
47
  sig { override.returns(T.nilable(Gem::Version)) }
45
48
  def latest_resolvable_version
49
+ return latest_resolvable_version_for_git_dependency if git_dependency?
50
+
46
51
  @latest_resolvable_version ||= T.let(
47
52
  if resolver_type == :requirements
48
53
  resolver.latest_resolvable_version
@@ -59,6 +64,8 @@ module Dependabot
59
64
 
60
65
  sig { override.returns(T.nilable(Gem::Version)) }
61
66
  def latest_resolvable_version_with_no_unlock
67
+ return T.cast(dependency.version, T.nilable(Gem::Version)) if git_dependency? && git_commit_checker.pinned?
68
+
62
69
  @latest_resolvable_version_with_no_unlock ||= T.let(
63
70
  if resolver_type == :requirements
64
71
  resolver.latest_resolvable_version_with_no_unlock
@@ -88,6 +95,8 @@ module Dependabot
88
95
 
89
96
  sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
90
97
  def updated_requirements
98
+ return updated_git_requirements if git_dependency?
99
+
91
100
  RequirementsUpdater.new(
92
101
  requirements: requirements,
93
102
  latest_resolvable_version: preferred_resolvable_version&.to_s,
@@ -112,6 +121,64 @@ module Dependabot
112
121
 
113
122
  private
114
123
 
124
+ sig { returns(T::Boolean) }
125
+ def git_dependency?
126
+ git_commit_checker.git_dependency?
127
+ end
128
+
129
+ sig { returns(T.nilable(Gem::Version)) }
130
+ def latest_version_for_git_dependency
131
+ latest_git_version_details&.fetch(:version)
132
+ end
133
+
134
+ sig { returns(T.nilable(Gem::Version)) }
135
+ def latest_resolvable_version_for_git_dependency
136
+ # For git dependencies, we assume the latest version is resolvable
137
+ latest_version_for_git_dependency
138
+ end
139
+
140
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
141
+ def updated_git_requirements
142
+ updated_source = updated_git_source
143
+ return requirements unless updated_source
144
+
145
+ requirements.map do |req|
146
+ req.merge(source: updated_source)
147
+ end
148
+ end
149
+
150
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
151
+ def updated_git_source
152
+ # Update the git tag if a new version is available
153
+ if git_commit_checker.pinned_ref_looks_like_version? && latest_git_version_details
154
+ new_tag = T.must(latest_git_version_details).fetch(:tag)
155
+ source_details = dependency.source_details
156
+ return source_details.transform_keys(&:to_sym).merge(ref: new_tag) if source_details
157
+ end
158
+
159
+ # Otherwise return the original source
160
+ dependency.source_details&.transform_keys(&:to_sym)
161
+ end
162
+
163
+ sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
164
+ def latest_git_version_details
165
+ @latest_git_version_details ||= T.let(
166
+ latest_version_finder.latest_version_tag(git_commit_checker: git_commit_checker),
167
+ T.nilable(T::Hash[Symbol, T.untyped])
168
+ )
169
+ end
170
+
171
+ sig { returns(Dependabot::GitCommitChecker) }
172
+ def git_commit_checker
173
+ @git_commit_checker ||= T.let(
174
+ Dependabot::GitCommitChecker.new(
175
+ dependency: dependency,
176
+ credentials: credentials
177
+ ),
178
+ T.nilable(Dependabot::GitCommitChecker)
179
+ )
180
+ end
181
+
115
182
  sig { override.returns(T::Boolean) }
116
183
  def latest_version_resolvable_with_full_unlock?
117
184
  # Full unlock checks aren't implemented for Python (yet)
@@ -135,10 +202,6 @@ module Dependabot
135
202
 
136
203
  sig { returns(T.untyped) }
137
204
  def resolver
138
- if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
139
- Dependabot.logger.info("Python package resolver : #{resolver_type}")
140
- end
141
-
142
205
  case resolver_type
143
206
  when :pip_compile then pip_compile_version_resolver
144
207
  when :pipenv then pipenv_version_resolver
@@ -456,6 +519,7 @@ module Dependabot
456
519
  dependency_files.select { |f| f.name.end_with?(".in") }
457
520
  end
458
521
  end
522
+ # rubocop:enable Metrics/ClassLength
459
523
  end
460
524
  end
461
525
 
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.357.0
4
+ version: 0.359.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.357.0
18
+ version: 0.359.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.357.0
25
+ version: 0.359.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -291,7 +291,7 @@ licenses:
291
291
  - MIT
292
292
  metadata:
293
293
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
294
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.357.0
294
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.359.0
295
295
  rdoc_options: []
296
296
  require_paths:
297
297
  - lib