dependabot-python 0.236.0 → 0.238.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9f6ef3950d4558611300b72d3839af555ea94e389aeb114a1257290901419e8
4
- data.tar.gz: c896fc1ec74464344a70a3bf00add5181424b599c4553bf1d8dcb1e9708d8fcc
3
+ metadata.gz: b05bd36f835c00c6533153183b23d848d6b06759d20dc4e643cc2fd5e9a8c5e6
4
+ data.tar.gz: 0c08d7fdb367c16636cd558dca7ad36e16bba8851b4f906ac076d8a28b45bbdb
5
5
  SHA512:
6
- metadata.gz: 0c25f7375610f6be67b98731d756af8c16b551971dfef4661f59e48fd3b8cfb09ac962b60458bcb99ddcc40c58a6246c5e95062eb9d3724d0d7d93be5860e71a
7
- data.tar.gz: 54538b41299885f7187c6c3fe8d2967eb2bf6001fdc18dfb897cc1b70841060de2b48f834e480406d4e8c595c5897d734856e549e9cfe04d820a66dee8e39c09
6
+ metadata.gz: 1a7faaff71e67be34ad8d152bb5f869313f69a7a031cf4ffe4f1b2e5cc565e40ca0d9e9e5e2c470ff5477b5ab8c166ac29fc2323ac2c7548ec7e684348fac98b
7
+ data.tar.gz: 78adf7610fff0b22a46ac7d105a6463e1040505de29ecb15e9b087930bdfe5ba54cf6755bdefbff610a9009ae6a6bac6ae59997cae050c9dae06cd31ec1eda0d
@@ -1,10 +1,10 @@
1
- pip==23.2.1
1
+ pip==23.3.1
2
2
  pip-tools==7.3.0
3
3
  flake8==6.1.0
4
4
  hashin==0.17.0
5
- pipenv==2023.8.28
5
+ pipenv@git+https://github.com/pypa/pipenv@main
6
6
  pipfile==0.0.2
7
- poetry==1.6.1
7
+ poetry==1.7.1
8
8
 
9
9
  # Some dependencies will only install if Cython is present
10
- Cython==3.0.3
10
+ Cython==3.0.5
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "toml-rb"
5
+ require "sorbet-runtime"
5
6
 
6
7
  require "dependabot/file_fetchers"
7
8
  require "dependabot/file_fetchers/base"
@@ -15,6 +16,9 @@ require "dependabot/errors"
15
16
  module Dependabot
16
17
  module Python
17
18
  class FileFetcher < Dependabot::FileFetchers::Base
19
+ extend T::Sig
20
+ extend T::Helpers
21
+
18
22
  CHILD_REQUIREMENT_REGEX = /^-r\s?(?<path>.*\.(?:txt|in))/
19
23
  CONSTRAINT_REGEX = /^-c\s?(?<path>.*\.(?:txt|in))/
20
24
  DEPENDENCY_TYPES = %w(packages dev-packages).freeze
@@ -63,8 +67,7 @@ module Dependabot
63
67
  }
64
68
  end
65
69
 
66
- private
67
-
70
+ sig { override.returns(T::Array[DependencyFile]) }
68
71
  def fetch_files
69
72
  fetched_files = []
70
73
 
@@ -84,6 +87,8 @@ module Dependabot
84
87
  uniq_files(fetched_files)
85
88
  end
86
89
 
90
+ private
91
+
87
92
  def uniq_files(fetched_files)
88
93
  uniq_files = fetched_files.reject(&:support_file?).uniq
89
94
  uniq_files += fetched_files
@@ -67,7 +67,8 @@ module Dependabot
67
67
  source: nil,
68
68
  groups: [group]
69
69
  }],
70
- package_manager: "pip"
70
+ package_manager: "pip",
71
+ metadata: { original_name: dep_name }
71
72
  )
72
73
  end
73
74
  end
@@ -137,12 +137,21 @@ module Dependabot
137
137
 
138
138
  check_requirements(requirement)
139
139
 
140
- {
141
- requirement: requirement.is_a?(String) ? requirement : requirement["version"],
142
- file: pyproject.name,
143
- source: nil,
144
- groups: [type]
145
- }
140
+ if requirement.is_a?(String)
141
+ {
142
+ requirement: requirement,
143
+ file: pyproject.name,
144
+ source: nil,
145
+ groups: [type]
146
+ }
147
+ else
148
+ {
149
+ requirement: requirement["version"],
150
+ file: pyproject.name,
151
+ source: requirement.fetch("source", nil),
152
+ groups: [type]
153
+ }
154
+ end
146
155
  end
147
156
  end
148
157
 
@@ -6,6 +6,7 @@ require "open3"
6
6
  require "dependabot/errors"
7
7
  require "dependabot/shared_helpers"
8
8
  require "dependabot/python/file_parser"
9
+ require "dependabot/python/pip_compile_file_matcher"
9
10
  require "dependabot/python/requirement"
10
11
 
11
12
  module Dependabot
@@ -22,6 +23,7 @@ module Dependabot
22
23
  [
23
24
  pipfile_python_requirement,
24
25
  pyproject_python_requirement,
26
+ pip_compile_python_requirement,
25
27
  python_version_file_version,
26
28
  runtime_file_python_version,
27
29
  setup_file_requirement
@@ -64,6 +66,20 @@ module Dependabot
64
66
  poetry_object&.dig("dev-dependencies", "python")
65
67
  end
66
68
 
69
+ def pip_compile_python_requirement
70
+ requirement_files.each do |file|
71
+ next unless pip_compile_file_matcher.lockfile_for_pip_compile_file?(file)
72
+
73
+ marker = /^# This file is autogenerated by pip-compile with [pP]ython (?<version>\d+.\d+)$/m
74
+ match = marker.match(file.content)
75
+ next unless match
76
+
77
+ return match[:version]
78
+ end
79
+
80
+ nil
81
+ end
82
+
67
83
  def python_version_file_version
68
84
  return unless python_version_file
69
85
 
@@ -106,6 +122,10 @@ module Dependabot
106
122
  SharedHelpers.run_shell_command(command, env: env, stderr_to_stdout: true)
107
123
  end
108
124
 
125
+ def pip_compile_file_matcher
126
+ @pip_compile_file_matcher ||= PipCompileFileMatcher.new(pip_compile_files)
127
+ end
128
+
109
129
  def requirement_class
110
130
  Dependabot::Python::Requirement
111
131
  end
@@ -144,6 +164,10 @@ module Dependabot
144
164
  def requirement_files
145
165
  dependency_files.select { |f| f.name.end_with?(".txt") }
146
166
  end
167
+
168
+ def pip_compile_files
169
+ dependency_files.select { |f| f.name.end_with?(".in") }
170
+ end
147
171
  end
148
172
  end
149
173
  end
@@ -1,7 +1,6 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "toml-rb"
5
4
  require "open3"
6
5
  require "dependabot/dependency"
7
6
  require "dependabot/python/requirement_parser"
@@ -10,7 +9,7 @@ require "dependabot/python/file_updater"
10
9
  require "dependabot/python/language_version_manager"
11
10
  require "dependabot/shared_helpers"
12
11
  require "dependabot/python/native_helpers"
13
- require "dependabot/python/name_normaliser"
12
+ require "dependabot/python/pipenv_runner"
14
13
 
15
14
  module Dependabot
16
15
  module Python
@@ -22,12 +21,13 @@ module Dependabot
22
21
 
23
22
  DEPENDENCY_TYPES = %w(packages dev-packages).freeze
24
23
 
25
- attr_reader :dependencies, :dependency_files, :credentials
24
+ attr_reader :dependencies, :dependency_files, :credentials, :repo_contents_path
26
25
 
27
- def initialize(dependencies:, dependency_files:, credentials:)
26
+ def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path:)
28
27
  @dependencies = dependencies
29
28
  @dependency_files = dependency_files
30
29
  @credentials = credentials
30
+ @repo_contents_path = repo_contents_path
31
31
  end
32
32
 
33
33
  def updated_dependency_files
@@ -83,7 +83,6 @@ module Dependabot
83
83
  return [] unless lockfile
84
84
 
85
85
  pipfile_lock_deps = parsed_lockfile[type]&.keys&.sort || []
86
- pipfile_lock_deps = pipfile_lock_deps.map { |n| normalise(n) }
87
86
  return [] unless pipfile_lock_deps.any?
88
87
 
89
88
  regex = RequirementParser::INSTALL_REQ_WITH_REQUIREMENT
@@ -94,7 +93,7 @@ module Dependabot
94
93
  requirements_files.select do |req_file|
95
94
  deps = []
96
95
  req_file.content.scan(regex) { deps << Regexp.last_match }
97
- deps = deps.map { |m| normalise(m[:name]) }
96
+ deps = deps.map { |m| m[:name] }
98
97
  deps.sort == pipfile_lock_deps
99
98
  end
100
99
  end
@@ -129,61 +128,17 @@ module Dependabot
129
128
 
130
129
  def prepared_pipfile_content
131
130
  content = updated_pipfile_content
132
- content = freeze_other_dependencies(content)
133
- content = freeze_dependencies_being_updated(content)
134
131
  content = add_private_sources(content)
135
132
  content = update_python_requirement(content)
136
133
  content
137
134
  end
138
135
 
139
- def freeze_other_dependencies(pipfile_content)
140
- PipfilePreparer
141
- .new(pipfile_content: pipfile_content, lockfile: lockfile)
142
- .freeze_top_level_dependencies_except(dependencies)
143
- end
144
-
145
136
  def update_python_requirement(pipfile_content)
146
137
  PipfilePreparer
147
138
  .new(pipfile_content: pipfile_content)
148
139
  .update_python_requirement(language_version_manager.python_major_minor)
149
140
  end
150
141
 
151
- # rubocop:disable Metrics/PerceivedComplexity
152
- def freeze_dependencies_being_updated(pipfile_content)
153
- pipfile_object = TomlRB.parse(pipfile_content)
154
-
155
- dependencies.each do |dep|
156
- DEPENDENCY_TYPES.each do |type|
157
- names = pipfile_object[type]&.keys || []
158
- pkg_name = names.find { |nm| normalise(nm) == dep.name }
159
- next unless pkg_name || subdep_type?(type)
160
-
161
- pkg_name ||= dependency.name
162
- if pipfile_object[type][pkg_name].is_a?(Hash)
163
- pipfile_object[type][pkg_name]["version"] =
164
- "==#{dep.version}"
165
- else
166
- pipfile_object[type][pkg_name] = "==#{dep.version}"
167
- end
168
- end
169
- end
170
-
171
- TomlRB.dump(pipfile_object)
172
- end
173
- # rubocop:enable Metrics/PerceivedComplexity
174
-
175
- def subdep_type?(type)
176
- return false if dependency.top_level?
177
-
178
- lockfile_type = Python::FileParser::DEPENDENCY_GROUP_KEYS
179
- .find { |i| i.fetch(:pipfile) == type }
180
- .fetch(:lockfile)
181
-
182
- JSON.parse(lockfile.content)
183
- .fetch(lockfile_type, {})
184
- .keys.any? { |k| normalise(k) == dependency.name }
185
- end
186
-
187
142
  def add_private_sources(pipfile_content)
188
143
  PipfilePreparer
189
144
  .new(pipfile_content: pipfile_content)
@@ -192,14 +147,12 @@ module Dependabot
192
147
 
193
148
  def updated_generated_files
194
149
  @updated_generated_files ||=
195
- SharedHelpers.in_a_temporary_directory do
150
+ SharedHelpers.in_a_temporary_repo_directory(dependency_files.first.directory, repo_contents_path) do
196
151
  SharedHelpers.with_git_configured(credentials: credentials) do
197
152
  write_temporary_dependency_files(prepared_pipfile_content)
198
153
  install_required_python
199
154
 
200
- run_pipenv_command(
201
- "pyenv exec pipenv lock"
202
- )
155
+ pipenv_runner.run_upgrade("==#{dependency.version}")
203
156
 
204
157
  result = { lockfile: File.read("Pipfile.lock") }
205
158
  result[:lockfile] = post_process_lockfile(result[:lockfile])
@@ -245,17 +198,12 @@ module Dependabot
245
198
  File.write("dev-req.txt", dev_req_content)
246
199
  end
247
200
 
248
- def run_command(command, env: {}, fingerprint: nil)
249
- SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint)
201
+ def run_command(command)
202
+ SharedHelpers.run_shell_command(command)
250
203
  end
251
204
 
252
- def run_pipenv_command(command, env: pipenv_env_variables)
253
- run_command(
254
- "pyenv local #{language_version_manager.python_major_minor}",
255
- fingerprint: "pyenv local <python_major_minor>"
256
- )
257
-
258
- run_command(command, env: env)
205
+ def run_pipenv_command(command)
206
+ pipenv_runner.run(command)
259
207
  end
260
208
 
261
209
  def write_temporary_dependency_files(pipfile_content)
@@ -317,7 +265,7 @@ module Dependabot
317
265
  SharedHelpers.run_helper_subprocess(
318
266
  command: "pyenv exec python3 #{NativeHelpers.python_helper_path}",
319
267
  function: "get_pipfile_hash",
320
- args: [dir]
268
+ args: [T.cast(dir, Pathname).to_s]
321
269
  )
322
270
  end
323
271
  end
@@ -328,10 +276,6 @@ module Dependabot
328
276
  updated_file
329
277
  end
330
278
 
331
- def normalise(name)
332
- NameNormaliser.normalise(name)
333
- end
334
-
335
279
  def python_requirement_parser
336
280
  @python_requirement_parser ||=
337
281
  FileParser::PythonRequirementParser.new(
@@ -346,6 +290,15 @@ module Dependabot
346
290
  )
347
291
  end
348
292
 
293
+ def pipenv_runner
294
+ @pipenv_runner ||=
295
+ PipenvRunner.new(
296
+ dependency: dependency,
297
+ lockfile: lockfile,
298
+ language_version_manager: language_version_manager
299
+ )
300
+ end
301
+
349
302
  def parsed_lockfile
350
303
  @parsed_lockfile ||= JSON.parse(lockfile.content)
351
304
  end
@@ -369,16 +322,6 @@ module Dependabot
369
322
  def requirements_files
370
323
  dependency_files.select { |f| f.name.end_with?(".txt") }
371
324
  end
372
-
373
- def pipenv_env_variables
374
- {
375
- "PIPENV_YES" => "true", # Install new Python ver if needed
376
- "PIPENV_MAX_RETRIES" => "3", # Retry timeouts
377
- "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
378
- "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
379
- "PIP_DEFAULT_TIMEOUT" => "60" # Set pip timeout to 1 minute
380
- }
381
- end
382
325
  end
383
326
  end
384
327
  end
@@ -7,15 +7,13 @@ require "dependabot/dependency"
7
7
  require "dependabot/python/file_parser"
8
8
  require "dependabot/python/file_updater"
9
9
  require "dependabot/python/authed_url_builder"
10
- require "dependabot/python/name_normaliser"
11
10
 
12
11
  module Dependabot
13
12
  module Python
14
13
  class FileUpdater
15
14
  class PipfilePreparer
16
- def initialize(pipfile_content:, lockfile: nil)
15
+ def initialize(pipfile_content:)
17
16
  @pipfile_content = pipfile_content
18
- @lockfile = lockfile
19
17
  end
20
18
 
21
19
  def replace_sources(credentials)
@@ -28,45 +26,6 @@ module Dependabot
28
26
  TomlRB.dump(pipfile_object)
29
27
  end
30
28
 
31
- def freeze_top_level_dependencies_except(dependencies)
32
- return pipfile_content unless lockfile
33
-
34
- pipfile_object = TomlRB.parse(pipfile_content)
35
- excluded_names = dependencies.map(&:name)
36
-
37
- Python::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
38
- next unless pipfile_object[keys[:pipfile]]
39
-
40
- pipfile_object.fetch(keys[:pipfile]).each do |dep_name, _|
41
- next if excluded_names.include?(normalise(dep_name))
42
-
43
- freeze_dependency(dep_name, pipfile_object, keys)
44
- end
45
- end
46
-
47
- TomlRB.dump(pipfile_object)
48
- end
49
-
50
- def freeze_dependency(dep_name, pipfile_object, keys)
51
- locked_version = version_from_lockfile(
52
- keys[:lockfile],
53
- normalise(dep_name)
54
- )
55
- locked_ref = ref_from_lockfile(
56
- keys[:lockfile],
57
- normalise(dep_name)
58
- )
59
-
60
- pipfile_req = pipfile_object[keys[:pipfile]][dep_name]
61
- if pipfile_req.is_a?(Hash) && locked_version
62
- pipfile_req["version"] = "==#{locked_version}"
63
- elsif pipfile_req.is_a?(Hash) && locked_ref && !pipfile_req["ref"]
64
- pipfile_req["ref"] = locked_ref
65
- elsif locked_version
66
- pipfile_object[keys[:pipfile]][dep_name] = "==#{locked_version}"
67
- end
68
- end
69
-
70
29
  def update_python_requirement(requirement)
71
30
  pipfile_object = TomlRB.parse(pipfile_content)
72
31
 
@@ -84,31 +43,6 @@ module Dependabot
84
43
 
85
44
  attr_reader :pipfile_content, :lockfile
86
45
 
87
- def version_from_lockfile(dep_type, dep_name)
88
- details = parsed_lockfile.dig(dep_type, normalise(dep_name))
89
-
90
- case details
91
- when String then details.gsub(/^==/, "")
92
- when Hash then details["version"]&.gsub(/^==/, "")
93
- end
94
- end
95
-
96
- def ref_from_lockfile(dep_type, dep_name)
97
- details = parsed_lockfile.dig(dep_type, normalise(dep_name))
98
-
99
- case details
100
- when Hash then details["ref"]
101
- end
102
- end
103
-
104
- def parsed_lockfile
105
- @parsed_lockfile ||= JSON.parse(lockfile.content)
106
- end
107
-
108
- def normalise(name)
109
- NameNormaliser.normalise(name)
110
- end
111
-
112
46
  def pipfile_sources
113
47
  @pipfile_sources ||= TomlRB.parse(pipfile_content).fetch("source", [])
114
48
  end
@@ -238,7 +238,7 @@ module Dependabot
238
238
  SharedHelpers.run_helper_subprocess(
239
239
  command: "pyenv exec python3 #{python_helper_path}",
240
240
  function: "get_pyproject_hash",
241
- args: [dir]
241
+ args: [T.cast(dir, Pathname).to_s]
242
242
  )
243
243
  end
244
244
  end
@@ -247,7 +247,8 @@ module Dependabot
247
247
  def declaration_regex(dep, old_req)
248
248
  group = old_req[:groups].first
249
249
 
250
- /#{group}(?:\.dependencies)?\]\s*\n.*?(?<declaration>(?:^\s*|["'])#{escape(dep)}["']?\s*=[^\n]*)$/mi
250
+ header_regex = "#{group}(?:\\.dependencies)?\\]\s*(?:\s*#.*?)*?"
251
+ /#{header_regex}\n.*?(?<declaration>(?:^\s*|["'])#{escape(dep)}["']?\s*=[^\n]*)$/mi
251
252
  end
252
253
 
253
254
  def table_declaration_regex(dep, old_req)
@@ -88,7 +88,8 @@ module Dependabot
88
88
  PipfileFileUpdater.new(
89
89
  dependencies: dependencies,
90
90
  dependency_files: dependency_files,
91
- credentials: credentials
91
+ credentials: credentials,
92
+ repo_contents_path: repo_contents_path
92
93
  ).updated_dependency_files
93
94
  end
94
95
 
@@ -0,0 +1,84 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/shared_helpers"
5
+ require "dependabot/python/file_parser"
6
+ require "json"
7
+
8
+ module Dependabot
9
+ module Python
10
+ class PipenvRunner
11
+ def initialize(dependency:, lockfile:, language_version_manager:)
12
+ @dependency = dependency
13
+ @lockfile = lockfile
14
+ @language_version_manager = language_version_manager
15
+ end
16
+
17
+ def run_upgrade(constraint)
18
+ constraint = "" if constraint == "*"
19
+ command = "pyenv exec pipenv upgrade --verbose #{dependency_name}#{constraint}"
20
+ command << " --dev" if lockfile_section == "develop"
21
+
22
+ run(command, fingerprint: "pyenv exec pipenv upgrade --verbose <dependency_name><constraint>")
23
+ end
24
+
25
+ def run_upgrade_and_fetch_version(constraint)
26
+ run_upgrade(constraint)
27
+
28
+ updated_lockfile = JSON.parse(File.read("Pipfile.lock"))
29
+
30
+ fetch_version_from_parsed_lockfile(updated_lockfile)
31
+ end
32
+
33
+ def run(command, fingerprint: nil)
34
+ run_command(
35
+ "pyenv local #{language_version_manager.python_major_minor}",
36
+ fingerprint: "pyenv local <python_major_minor>"
37
+ )
38
+
39
+ run_command(command, fingerprint: fingerprint)
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :dependency, :lockfile, :language_version_manager
45
+
46
+ def fetch_version_from_parsed_lockfile(updated_lockfile)
47
+ deps = updated_lockfile[lockfile_section] || {}
48
+
49
+ deps.dig(dependency_name, "version")
50
+ &.gsub(/^==/, "")
51
+ end
52
+
53
+ def run_command(command, fingerprint: nil)
54
+ SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
55
+ end
56
+
57
+ def lockfile_section
58
+ if dependency.requirements.any?
59
+ dependency.requirements.first[:groups].first
60
+ else
61
+ Python::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
62
+ section = keys.fetch(:lockfile)
63
+ return section if JSON.parse(lockfile.content)[section].keys.any?(dependency_name)
64
+ end
65
+ end
66
+ end
67
+
68
+ def dependency_name
69
+ dependency.metadata[:original_name] || dependency.name
70
+ end
71
+
72
+ def pipenv_env_variables
73
+ {
74
+ "PIPENV_YES" => "true", # Install new Python ver if needed
75
+ "PIPENV_MAX_RETRIES" => "3", # Retry timeouts
76
+ "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
77
+ "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
78
+ "PIP_DEFAULT_TIMEOUT" => "60", # Set pip timeout to 1 minute
79
+ "COLUMNS" => "250" # Avoid line wrapping
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -12,9 +12,10 @@ module Dependabot
12
12
  PYPI_BASE_URL = "https://pypi.org/simple/"
13
13
  ENVIRONMENT_VARIABLE_REGEX = /\$\{.+\}/
14
14
 
15
- def initialize(dependency_files:, credentials:)
15
+ def initialize(dependency_files:, credentials:, dependency:)
16
16
  @dependency_files = dependency_files
17
17
  @credentials = credentials
18
+ @dependency = dependency
18
19
  end
19
20
 
20
21
  def index_urls
@@ -122,9 +123,13 @@ module Dependabot
122
123
  # If source is PyPI, skip it, and let it pick the default URI
123
124
  next if source["name"].casecmp?("PyPI")
124
125
 
125
- if source["default"]
126
+ if @dependency.all_sources.include?(source["name"])
127
+ # If dependency has specified this source, use it
128
+ return { main: source["url"], extra: [] }
129
+ elsif source["default"]
126
130
  urls[:main] = source["url"]
127
- else
131
+ elsif source["priority"] != "explicit"
132
+ # if source is not explicit, add it to extra
128
133
  urls[:extra] << source["url"]
129
134
  end
130
135
  end
@@ -213,7 +213,8 @@ module Dependabot
213
213
  @index_urls ||=
214
214
  IndexFinder.new(
215
215
  dependency_files: dependency_files,
216
- credentials: credentials
216
+ credentials: credentials,
217
+ dependency: dependency
217
218
  ).index_urls
218
219
  end
219
220
 
@@ -33,12 +33,13 @@ module Dependabot
33
33
  RESOLUTION_IMPOSSIBLE_ERROR = "ResolutionImpossible"
34
34
  ERROR_REGEX = /(?<=ERROR\:\W).*$/
35
35
 
36
- attr_reader :dependency, :dependency_files, :credentials
36
+ attr_reader :dependency, :dependency_files, :credentials, :repo_contents_path
37
37
 
38
- def initialize(dependency:, dependency_files:, credentials:)
38
+ def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:)
39
39
  @dependency = dependency
40
40
  @dependency_files = dependency_files
41
41
  @credentials = credentials
42
+ @repo_contents_path = repo_contents_path
42
43
  @build_isolation = true
43
44
  end
44
45
 
@@ -1,8 +1,7 @@
1
- # typed: false
1
+ # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
5
- require "toml-rb"
6
5
  require "open3"
7
6
  require "dependabot/dependency"
8
7
  require "dependabot/errors"
@@ -13,23 +12,14 @@ require "dependabot/python/file_updater/pipfile_preparer"
13
12
  require "dependabot/python/file_updater/setup_file_sanitizer"
14
13
  require "dependabot/python/update_checker"
15
14
  require "dependabot/python/native_helpers"
16
- require "dependabot/python/name_normaliser"
15
+ require "dependabot/python/pipenv_runner"
17
16
  require "dependabot/python/version"
18
17
 
19
18
  module Dependabot
20
19
  module Python
21
20
  class UpdateChecker
22
- # This class does version resolution for Pipfiles. Its current approach
23
- # is somewhat crude:
24
- # - Unlock the dependency we're checking in the Pipfile
25
- # - Freeze all of the other dependencies in the Pipfile
26
- # - Run `pipenv lock` and see what the result is
27
- #
28
- # Unfortunately, Pipenv doesn't resolve how we'd expect - it appears to
29
- # just raise if the latest version can't be resolved. Knowing that is
30
- # still better than nothing, though.
31
21
  class PipenvVersionResolver
32
- GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none (?<url>[^\s]+).*/
22
+ GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none --quiet (?<url>[^\s]+).*/
33
23
  GIT_REFERENCE_NOT_FOUND_REGEX = /git checkout -q (?<tag>[^\s]+).*/
34
24
  PIPENV_INSTALLATION_ERROR = "python setup.py egg_info exited with 1"
35
25
  PIPENV_INSTALLATION_ERROR_REGEX =
@@ -37,14 +27,13 @@ module Dependabot
37
27
 
38
28
  PIPENV_RANGE_WARNING = /Warning:\sPython\s[<>].* was not found/
39
29
 
40
- DEPENDENCY_TYPES = %w(packages dev-packages).freeze
30
+ attr_reader :dependency, :dependency_files, :credentials, :repo_contents_path
41
31
 
42
- attr_reader :dependency, :dependency_files, :credentials
43
-
44
- def initialize(dependency:, dependency_files:, credentials:)
32
+ def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:)
45
33
  @dependency = dependency
46
34
  @dependency_files = dependency_files
47
35
  @credentials = credentials
36
+ @repo_contents_path = repo_contents_path
48
37
  end
49
38
 
50
39
  def latest_resolvable_version(requirement: nil)
@@ -68,53 +57,18 @@ module Dependabot
68
57
  return @latest_resolvable_version_string[requirement] if @latest_resolvable_version_string.key?(requirement)
69
58
 
70
59
  @latest_resolvable_version_string[requirement] ||=
71
- SharedHelpers.in_a_temporary_directory do
60
+ SharedHelpers.in_a_temporary_repo_directory(base_directory, repo_contents_path) do
72
61
  SharedHelpers.with_git_configured(credentials: credentials) do
73
- write_temporary_dependency_files(updated_req: requirement)
62
+ write_temporary_dependency_files
74
63
  install_required_python
75
64
 
76
- # Shell out to Pipenv, which handles everything for us.
77
- # Whilst calling `lock` avoids doing an install as part of the
78
- # pipenv flow, an install is still done by pip-tools in order
79
- # to resolve the dependencies. That means this is slow.
80
- run_pipenv_command("pyenv exec pipenv lock")
81
-
82
- updated_lockfile = JSON.parse(File.read("Pipfile.lock"))
83
-
84
- fetch_version_from_parsed_lockfile(updated_lockfile)
65
+ pipenv_runner.run_upgrade_and_fetch_version(requirement)
85
66
  end
86
67
  rescue SharedHelpers::HelperSubprocessFailed => e
87
68
  handle_pipenv_errors(e)
88
69
  end
89
70
  end
90
71
 
91
- def fetch_version_from_parsed_lockfile(updated_lockfile)
92
- if dependency.requirements.any?
93
- group = dependency.requirements.first[:groups].first
94
- deps = updated_lockfile[group] || {}
95
-
96
- version =
97
- deps.transform_keys { |k| normalise(k) }
98
- .dig(dependency.name, "version")
99
- &.gsub(/^==/, "")
100
-
101
- return version
102
- end
103
-
104
- Python::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
105
- deps = updated_lockfile[keys.fetch(:lockfile)] || {}
106
- version =
107
- deps.transform_keys { |k| normalise(k) }
108
- .dig(dependency.name, "version")
109
- &.gsub(/^==/, "")
110
-
111
- return version if version
112
- end
113
-
114
- # If the sub-dependency no longer appears in the lockfile return nil
115
- nil
116
- end
117
-
118
72
  # rubocop:disable Metrics/CyclomaticComplexity
119
73
  # rubocop:disable Metrics/PerceivedComplexity
120
74
  # rubocop:disable Metrics/AbcSize
@@ -136,6 +90,19 @@ module Dependabot
136
90
  raise DependencyFileNotResolvable, msg
137
91
  end
138
92
 
93
+ if error.message.match?(GIT_REFERENCE_NOT_FOUND_REGEX)
94
+ tag = error.message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag")
95
+ # Unfortunately the error message doesn't include the package name.
96
+ # TODO: Talk with pipenv maintainers about exposing the package name, it used to be part of the error output
97
+ raise GitDependencyReferenceNotFound, "(unknown package at #{tag})"
98
+ end
99
+
100
+ if error.message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX)
101
+ url = error.message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX)
102
+ .named_captures.fetch("url")
103
+ raise GitDependenciesNotReachable, url
104
+ end
105
+
139
106
  if error.message.include?("Could not find a version") || error.message.include?("ResolutionFailure")
140
107
  check_original_requirements_resolvable
141
108
  end
@@ -165,20 +132,7 @@ module Dependabot
165
132
  return if error.message.match?(/#{Regexp.quote(dependency.name)}/i)
166
133
  end
167
134
 
168
- if error.message.match?(GIT_REFERENCE_NOT_FOUND_REGEX)
169
- tag = error.message.match(GIT_REFERENCE_NOT_FOUND_REGEX).named_captures.fetch("tag")
170
- # Unfortunately the error message doesn't include the package name.
171
- # TODO: Talk with pipenv maintainers about exposing the package name, it used to be part of the error output
172
- raise GitDependencyReferenceNotFound, "(unknown package at #{tag})"
173
- end
174
-
175
- if error.message.match?(GIT_DEPENDENCY_UNREACHABLE_REGEX)
176
- url = error.message.match(GIT_DEPENDENCY_UNREACHABLE_REGEX)
177
- .named_captures.fetch("url")
178
- raise GitDependenciesNotReachable, url
179
- end
180
-
181
- raise unless error.message.include?("could not be resolved")
135
+ raise unless error.message.include?("ResolutionFailure")
182
136
  end
183
137
  # rubocop:enable Metrics/CyclomaticComplexity
184
138
  # rubocop:enable Metrics/PerceivedComplexity
@@ -190,10 +144,10 @@ module Dependabot
190
144
  # boolean, so that all deps for this repo will raise identical
191
145
  # errors when failing to update
192
146
  def check_original_requirements_resolvable
193
- SharedHelpers.in_a_temporary_directory do
147
+ SharedHelpers.in_a_temporary_repo_directory(base_directory, repo_contents_path) do
194
148
  write_temporary_dependency_files(update_pipfile: false)
195
149
 
196
- run_pipenv_command("pyenv exec pipenv lock")
150
+ pipenv_runner.run_upgrade("==#{dependency.version}")
197
151
 
198
152
  true
199
153
  rescue SharedHelpers::HelperSubprocessFailed => e
@@ -201,6 +155,10 @@ module Dependabot
201
155
  end
202
156
  end
203
157
 
158
+ def base_directory
159
+ dependency_files.first.directory
160
+ end
161
+
204
162
  def handle_pipenv_errors_resolving_original_reqs(error)
205
163
  if error.message.include?("Could not find a version") ||
206
164
  error.message.include?("package versions have conflicting dependencies")
@@ -220,10 +178,6 @@ module Dependabot
220
178
  raise DependencyFileNotResolvable, msg
221
179
  end
222
180
 
223
- # NOTE: Pipenv masks the actual error, see this issue for updates:
224
- # https://github.com/pypa/pipenv/issues/2791
225
- # TODO: This may no longer be reproducible on latest pipenv, see linked issue,
226
- # so investigate when we next bump to newer pipenv...
227
181
  handle_pipenv_installation_error(error.message) if error.message.match?(PIPENV_INSTALLATION_ERROR_REGEX)
228
182
 
229
183
  # Raise an unhandled error, as this could be a problem with
@@ -264,8 +218,7 @@ module Dependabot
264
218
  raise DependencyFileNotResolvable, msg
265
219
  end
266
220
 
267
- def write_temporary_dependency_files(updated_req: nil,
268
- update_pipfile: true)
221
+ def write_temporary_dependency_files(update_pipfile: true)
269
222
  dependency_files.each do |file|
270
223
  path = file.name
271
224
  FileUtils.mkdir_p(Pathname.new(path).dirname)
@@ -291,7 +244,7 @@ module Dependabot
291
244
  # Overwrite the pipfile with updated content
292
245
  File.write(
293
246
  "Pipfile",
294
- pipfile_content(updated_requirement: updated_req)
247
+ pipfile_content
295
248
  )
296
249
  end
297
250
 
@@ -319,93 +272,27 @@ module Dependabot
319
272
  dependency_files.find { |f| f.name == config_name }
320
273
  end
321
274
 
322
- def pipfile_content(updated_requirement:)
275
+ def pipfile_content
323
276
  content = pipfile.content
324
- content = freeze_other_dependencies(content)
325
- content = set_target_dependency_req(content, updated_requirement)
326
277
  content = add_private_sources(content)
327
278
  content = update_python_requirement(content)
328
279
  content
329
280
  end
330
281
 
331
- def freeze_other_dependencies(pipfile_content)
332
- Python::FileUpdater::PipfilePreparer
333
- .new(pipfile_content: pipfile_content, lockfile: lockfile)
334
- .freeze_top_level_dependencies_except([dependency])
335
- end
336
-
337
282
  def update_python_requirement(pipfile_content)
338
283
  Python::FileUpdater::PipfilePreparer
339
284
  .new(pipfile_content: pipfile_content)
340
285
  .update_python_requirement(language_version_manager.python_major_minor)
341
286
  end
342
287
 
343
- # rubocop:disable Metrics/PerceivedComplexity
344
- def set_target_dependency_req(pipfile_content, updated_requirement)
345
- return pipfile_content unless updated_requirement
346
-
347
- pipfile_object = TomlRB.parse(pipfile_content)
348
-
349
- DEPENDENCY_TYPES.each do |type|
350
- names = pipfile_object[type]&.keys || []
351
- pkg_name = names.find { |nm| normalise(nm) == dependency.name }
352
- next unless pkg_name || subdep_type?(type)
353
-
354
- pkg_name ||= dependency.name
355
- if pipfile_object.dig(type, pkg_name).is_a?(Hash)
356
- pipfile_object[type][pkg_name]["version"] = updated_requirement
357
- else
358
- pipfile_object[type][pkg_name] = updated_requirement
359
- end
360
- end
361
-
362
- TomlRB.dump(pipfile_object)
363
- end
364
- # rubocop:enable Metrics/PerceivedComplexity
365
-
366
- def subdep_type?(type)
367
- return false if dependency.top_level?
368
-
369
- lockfile_type = Python::FileParser::DEPENDENCY_GROUP_KEYS
370
- .find { |i| i.fetch(:pipfile) == type }
371
- .fetch(:lockfile)
372
-
373
- JSON.parse(lockfile.content)
374
- .fetch(lockfile_type, {})
375
- .keys.any? { |k| normalise(k) == dependency.name }
376
- end
377
-
378
288
  def add_private_sources(pipfile_content)
379
289
  Python::FileUpdater::PipfilePreparer
380
290
  .new(pipfile_content: pipfile_content)
381
291
  .replace_sources(credentials)
382
292
  end
383
293
 
384
- def run_command(command, env: {}, fingerprint: nil)
385
- SharedHelpers.run_shell_command(command, env: env, fingerprint: fingerprint, stderr_to_stdout: true)
386
- end
387
-
388
- def run_pipenv_command(command, env: pipenv_env_variables)
389
- run_command(
390
- "pyenv local #{language_version_manager.python_major_minor}",
391
- fingerprint: "pyenv local <python_major_minor>"
392
- )
393
-
394
- run_command(command, env: env)
395
- end
396
-
397
- def pipenv_env_variables
398
- {
399
- "PIPENV_YES" => "true", # Install new Python ver if needed
400
- "PIPENV_MAX_RETRIES" => "3", # Retry timeouts
401
- "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
402
- "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
403
- "PIP_DEFAULT_TIMEOUT" => "60" # Set pip timeout to 1 minute
404
- }
405
- end
406
-
407
- def normalise(name)
408
- NameNormaliser.normalise(name)
294
+ def run_command(command)
295
+ SharedHelpers.run_shell_command(command, stderr_to_stdout: true)
409
296
  end
410
297
 
411
298
  def python_requirement_parser
@@ -422,6 +309,15 @@ module Dependabot
422
309
  )
423
310
  end
424
311
 
312
+ def pipenv_runner
313
+ @pipenv_runner ||=
314
+ PipenvRunner.new(
315
+ dependency: dependency,
316
+ lockfile: lockfile,
317
+ language_version_manager: language_version_manager
318
+ )
319
+ end
320
+
425
321
  def pipfile
426
322
  dependency_files.find { |f| f.name == "Pipfile" }
427
323
  end
@@ -38,12 +38,13 @@ module Dependabot
38
38
  \s+check\syour\sgit\sconfiguration
39
39
  /mx
40
40
 
41
- attr_reader :dependency, :dependency_files, :credentials
41
+ attr_reader :dependency, :dependency_files, :credentials, :repo_contents_path
42
42
 
43
- def initialize(dependency:, dependency_files:, credentials:)
43
+ def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:)
44
44
  @dependency = dependency
45
45
  @dependency_files = dependency_files
46
46
  @credentials = credentials
47
+ @repo_contents_path = repo_contents_path
47
48
  end
48
49
 
49
50
  def latest_resolvable_version(requirement: nil)
@@ -191,7 +191,8 @@ module Dependabot
191
191
  {
192
192
  dependency: dependency,
193
193
  dependency_files: dependency_files,
194
- credentials: credentials
194
+ credentials: credentials,
195
+ repo_contents_path: repo_contents_path
195
196
  }
196
197
  end
197
198
 
@@ -221,11 +222,11 @@ module Dependabot
221
222
  return lower_bound_req if latest_version.nil?
222
223
  return lower_bound_req unless Python::Version.correct?(latest_version)
223
224
 
224
- lower_bound_req + ", <= #{latest_version}"
225
+ lower_bound_req + ",<=#{latest_version}"
225
226
  end
226
227
 
227
228
  def updated_version_req_lower_bound
228
- return ">= #{dependency.version}" if dependency.version
229
+ return ">=#{dependency.version}" if dependency.version
229
230
 
230
231
  version_for_requirement =
231
232
  requirements.filter_map { |r| r[:requirement] }
@@ -235,7 +236,7 @@ module Dependabot
235
236
  .select { |version| Gem::Version.correct?(version) }
236
237
  .max_by { |version| Gem::Version.new(version) }
237
238
 
238
- ">= #{version_for_requirement || 0}"
239
+ ">=#{version_for_requirement || 0}"
239
240
  end
240
241
 
241
242
  def fetch_latest_version
@@ -33,3 +33,6 @@ Dependabot::Dependency.register_name_normaliser(
33
33
  "pip",
34
34
  ->(name) { Dependabot::Python::NameNormaliser.normalise(name) }
35
35
  )
36
+
37
+ require "dependabot/utils"
38
+ Dependabot::Utils.register_always_clone("pip")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-python
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.236.0
4
+ version: 0.238.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-26 00:00:00.000000000 Z
11
+ date: 2023-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.236.0
19
+ version: 0.238.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.236.0
26
+ version: 0.238.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -94,20 +94,34 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec-sorbet
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.9.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.9.2
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rubocop
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: 1.56.0
117
+ version: 1.57.2
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: 1.56.0
124
+ version: 1.57.2
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: rubocop-performance
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -229,6 +243,7 @@ files:
229
243
  - lib/dependabot/python/name_normaliser.rb
230
244
  - lib/dependabot/python/native_helpers.rb
231
245
  - lib/dependabot/python/pip_compile_file_matcher.rb
246
+ - lib/dependabot/python/pipenv_runner.rb
232
247
  - lib/dependabot/python/requirement.rb
233
248
  - lib/dependabot/python/requirement_parser.rb
234
249
  - lib/dependabot/python/update_checker.rb
@@ -245,7 +260,7 @@ licenses:
245
260
  - Nonstandard
246
261
  metadata:
247
262
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
248
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.236.0
263
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.238.0
249
264
  post_install_message:
250
265
  rdoc_options: []
251
266
  require_paths: