dependabot-python 0.236.0 → 0.237.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: 1cd7a4517e826fb42d55e5f504f0162c5e0850b9d1dd01ff259451a40103cf8f
4
+ data.tar.gz: 9c064acaf52f7856f60881873d7d3127623d12935936c2b324d1beb2e1b6abc4
5
5
  SHA512:
6
- metadata.gz: 0c25f7375610f6be67b98731d756af8c16b551971dfef4661f59e48fd3b8cfb09ac962b60458bcb99ddcc40c58a6246c5e95062eb9d3724d0d7d93be5860e71a
7
- data.tar.gz: 54538b41299885f7187c6c3fe8d2967eb2bf6001fdc18dfb897cc1b70841060de2b48f834e480406d4e8c595c5897d734856e549e9cfe04d820a66dee8e39c09
6
+ metadata.gz: 175a621875538a2b4c7aab6b02a61cf89d8fa0847d4568466526d2403cfb67a2944322a39ecfef17834d08892e29099d3d54e00db124d6d3a8fb26d505616706
7
+ data.tar.gz: 0f6dc93dd2598d9f1ab8daf3e5c654c05ec3488c305ccec9a651de34680fefad8892d9d508c4752b74fa69767f4fc9514b9ca931b3f5a2ca87a2d773afedc5ec
@@ -1,4 +1,4 @@
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
@@ -7,4 +7,4 @@ pipfile==0.0.2
7
7
  poetry==1.6.1
8
8
 
9
9
  # Some dependencies will only install if Cython is present
10
- Cython==3.0.3
10
+ Cython==3.0.4
@@ -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
@@ -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,82 @@
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
+ command = "pyenv exec pipenv upgrade #{dependency_name}#{constraint}"
19
+ command << " --dev" if lockfile_section == "develop"
20
+
21
+ run(command, fingerprint: "pyenv exec pipenv upgrade <dependency_name><constraint>")
22
+ end
23
+
24
+ def run_upgrade_and_fetch_version(constraint)
25
+ run_upgrade(constraint)
26
+
27
+ updated_lockfile = JSON.parse(File.read("Pipfile.lock"))
28
+
29
+ fetch_version_from_parsed_lockfile(updated_lockfile)
30
+ end
31
+
32
+ def run(command, fingerprint: nil)
33
+ run_command(
34
+ "pyenv local #{language_version_manager.python_major_minor}",
35
+ fingerprint: "pyenv local <python_major_minor>"
36
+ )
37
+
38
+ run_command(command, fingerprint: fingerprint)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :dependency, :lockfile, :language_version_manager
44
+
45
+ def fetch_version_from_parsed_lockfile(updated_lockfile)
46
+ deps = updated_lockfile[lockfile_section] || {}
47
+
48
+ deps.dig(dependency_name, "version")
49
+ &.gsub(/^==/, "")
50
+ end
51
+
52
+ def run_command(command, fingerprint: nil)
53
+ SharedHelpers.run_shell_command(command, env: pipenv_env_variables, fingerprint: fingerprint)
54
+ end
55
+
56
+ def lockfile_section
57
+ if dependency.requirements.any?
58
+ dependency.requirements.first[:groups].first
59
+ else
60
+ Python::FileParser::DEPENDENCY_GROUP_KEYS.each do |keys|
61
+ section = keys.fetch(:lockfile)
62
+ return section if JSON.parse(lockfile.content)[section].keys.any?(dependency_name)
63
+ end
64
+ end
65
+ end
66
+
67
+ def dependency_name
68
+ dependency.metadata[:original_name] || dependency.name
69
+ end
70
+
71
+ def pipenv_env_variables
72
+ {
73
+ "PIPENV_YES" => "true", # Install new Python ver if needed
74
+ "PIPENV_MAX_RETRIES" => "3", # Retry timeouts
75
+ "PIPENV_NOSPIN" => "1", # Don't pollute logs with spinner
76
+ "PIPENV_TIMEOUT" => "600", # Set install timeout to 10 minutes
77
+ "PIP_DEFAULT_TIMEOUT" => "60" # Set pip timeout to 1 minute
78
+ }
79
+ end
80
+ end
81
+ end
82
+ 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
@@ -124,7 +125,11 @@ module Dependabot
124
125
 
125
126
  if source["default"]
126
127
  urls[:main] = source["url"]
127
- else
128
+ elsif source["priority"] != "explicit"
129
+ # if source is not explicit, add it to extra
130
+ urls[:extra] << source["url"]
131
+ elsif @dependency.all_sources.include?(source["name"])
132
+ # if source is explicit, and dependency has specified it as a source, 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
 
@@ -2,7 +2,6 @@
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,21 +12,12 @@ 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
22
  GIT_DEPENDENCY_UNREACHABLE_REGEX = /git clone --filter=blob:none (?<url>[^\s]+).*/
33
23
  GIT_REFERENCE_NOT_FOUND_REGEX = /git checkout -q (?<tag>[^\s]+).*/
@@ -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
@@ -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")
@@ -264,8 +222,7 @@ module Dependabot
264
222
  raise DependencyFileNotResolvable, msg
265
223
  end
266
224
 
267
- def write_temporary_dependency_files(updated_req: nil,
268
- update_pipfile: true)
225
+ def write_temporary_dependency_files(update_pipfile: true)
269
226
  dependency_files.each do |file|
270
227
  path = file.name
271
228
  FileUtils.mkdir_p(Pathname.new(path).dirname)
@@ -291,7 +248,7 @@ module Dependabot
291
248
  # Overwrite the pipfile with updated content
292
249
  File.write(
293
250
  "Pipfile",
294
- pipfile_content(updated_requirement: updated_req)
251
+ pipfile_content
295
252
  )
296
253
  end
297
254
 
@@ -319,93 +276,27 @@ module Dependabot
319
276
  dependency_files.find { |f| f.name == config_name }
320
277
  end
321
278
 
322
- def pipfile_content(updated_requirement:)
279
+ def pipfile_content
323
280
  content = pipfile.content
324
- content = freeze_other_dependencies(content)
325
- content = set_target_dependency_req(content, updated_requirement)
326
281
  content = add_private_sources(content)
327
282
  content = update_python_requirement(content)
328
283
  content
329
284
  end
330
285
 
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
286
  def update_python_requirement(pipfile_content)
338
287
  Python::FileUpdater::PipfilePreparer
339
288
  .new(pipfile_content: pipfile_content)
340
289
  .update_python_requirement(language_version_manager.python_major_minor)
341
290
  end
342
291
 
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
292
  def add_private_sources(pipfile_content)
379
293
  Python::FileUpdater::PipfilePreparer
380
294
  .new(pipfile_content: pipfile_content)
381
295
  .replace_sources(credentials)
382
296
  end
383
297
 
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)
298
+ def run_command(command)
299
+ SharedHelpers.run_shell_command(command, stderr_to_stdout: true)
409
300
  end
410
301
 
411
302
  def python_requirement_parser
@@ -422,6 +313,15 @@ module Dependabot
422
313
  )
423
314
  end
424
315
 
316
+ def pipenv_runner
317
+ @pipenv_runner ||=
318
+ PipenvRunner.new(
319
+ dependency: dependency,
320
+ lockfile: lockfile,
321
+ language_version_manager: language_version_manager
322
+ )
323
+ end
324
+
425
325
  def pipfile
426
326
  dependency_files.find { |f| f.name == "Pipfile" }
427
327
  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.237.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-11-21 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.237.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.237.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.237.0
249
264
  post_install_message:
250
265
  rdoc_options: []
251
266
  require_paths: