dependabot-python 0.236.0 → 0.237.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: 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: